Compare commits

..
128 changed files with 1032 additions and 1419 deletions
+1
View File
@@ -26,6 +26,7 @@ type FirewallSubsystemStatus struct {
}
type FirewallLifecycleOperation struct {
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
WithDockerRestart bool `json:"withDockerRestart"`
}
+6
View File
@@ -1,6 +1,7 @@
package dto
import (
"encoding/json"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -144,6 +145,11 @@ type MonitorVLLMCurrent struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
type MonitorVLLMCurrentData struct {
model.MonitorVLLM
Metrics map[string]json.RawMessage `json:"metrics,omitempty"`
}
type MonitorVLLMClean struct {
AppInstallID uint `json:"appInstallID" validate:"required"`
}
+2
View File
@@ -86,7 +86,9 @@ type MonitorVLLM struct {
RawMetrics string `json:"-"`
HistogramDeltas string `json:"-"`
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
SampleIntervalSeconds int `json:"-"`
RequestDeltas string `json:"-"`
CompletedRequests *float64 `json:"completedRequests" gorm:"-"`
QueueTime *float64 `json:"queueTime"`
AverageGenerationTokens *float64 `json:"averageGenerationTokens"`
PrefixCacheHitRate *float64 `json:"prefixCacheHitRate"`
+10 -2
View File
@@ -16,7 +16,8 @@ type VLLMMonitorRepo struct{}
type VLLMHistoryPoint struct {
model.MonitorVLLM
Bucket int64
Bucket int64
CompletedRequests *float64
}
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
@@ -77,12 +78,19 @@ func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.
}
db = db.Group("bucket").Order("bucket ASC")
} else {
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
columns = append([]string{"id", "created_at", "app_install_id", "status", "sample_interval_seconds"}, metrics...)
if group == "" || group == "requests" {
columns = append(columns, "histogram_deltas", "request_deltas")
}
db = db.Order("created_at ASC, id ASC")
}
if group == "" || group == "overview" {
count := "CASE WHEN json_valid(request_deltas) THEN CASE WHEN json_extract(request_deltas, '$.seconds') > 0 THEN ROUND(request_throughput * json_extract(request_deltas, '$.seconds')) END END"
if seconds > 0 {
count = "SUM(" + count + ")"
}
columns = append(columns, count+" AS completed_requests")
}
var points []VLLMHistoryPoint
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
return points, err
+47 -55
View File
@@ -131,6 +131,9 @@ func (s *FirewallService) LoadBaseInfo(chainGroup string) (dto.FirewallSubsystem
func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOperation) (dto.FirewallLifecycleOperationResponse, error) {
response := dto.FirewallLifecycleOperationResponse{}
if request.BackupFile != "" && request.Operation != string(lifecycle.OperationStart) {
return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw")
}
if request.Operation == "disableBanPing" || request.Operation == "enableBanPing" {
return response, s.OperateFirewall(request)
}
@@ -155,6 +158,19 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp
if err != nil {
return response, err
}
var restoreRequest dto.FirewallRuleCreate
if request.BackupFile != "" {
if client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW {
return response, fmt.Errorf("backup restoration is only supported when starting firewalld or ufw")
}
restoreRequest, err = prepareFirewallRuleCreate(dto.FirewallRuleCreate{BackupFile: request.BackupFile, Initialize: true})
if err != nil {
return response, err
}
if err := validateFirewallCreateBatch(restoreRequest, filter.Provider(client.Name())); err != nil {
return response, err
}
}
if (client.Name() != lifecycle.ProviderFirewalld && client.Name() != lifecycle.ProviderUFW) ||
(request.Operation != string(lifecycle.OperationStart) && request.Operation != string(lifecycle.OperationStop) &&
request.Operation != string(lifecycle.OperationRestart)) {
@@ -173,7 +189,26 @@ func (s *FirewallService) QueueFirewallOperation(request dto.FirewallLifecycleOp
return response, err
}
taskItem.AddSubTaskWithOps(name, func(t *task.Task) error {
return s.runFirewallLifecycleTask(t, client, request, true)
if request.BackupFile != "" {
if err := s.checkSelectedProvider(t.TaskCtx, filter.Provider(client.Name())); err != nil {
return err
}
}
prepareStart := func(lifecycle.Client) error {
if request.BackupFile != "" {
return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallCreateRulesStep"), func() error {
return s.runFirewallCreateRulesTask(t, restoreRequest, filter.Provider(client.Name()))
})
}
return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error {
err := s.SyncPortWhitelist(t.TaskCtx)
if err == nil && client.Name() == lifecycle.ProviderFirewalld {
err = lifecycleproviders.RemoveFirewalldSSHService()
}
return err
})
}
return s.runFirewallLifecycleTask(t, client, request, prepareStart)
}, nil, 0, 0)
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
closeUnstartedFirewallTask(taskItem)
@@ -554,29 +589,9 @@ func (s *FirewallService) LoadFirewallNativeDetail(ctx context.Context, request
}
func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCreate) (dto.FirewallRuleCreateResponse, error) {
if request.BackupFile != "" {
if len(request.Items) != 0 {
return dto.FirewallRuleCreateResponse{}, filter.ErrInvalidRule
}
items, err := readFirewallBackup(request.BackupFile)
if err != nil {
return dto.FirewallRuleCreateResponse{}, err
}
for _, item := range items {
request.Items = append(request.Items, dto.FirewallRuleCreateItem{Rule: item.FirewallRule, Raw: item.Raw, ParseStatus: item.ParseStatus, SourceKind: constant.FirewallRuleSourceImported})
}
}
for index, item := range request.Items {
if item.SourceKind != constant.FirewallRuleSourceImported || item.Rule.Scope.Provider != filter.ProviderUFW || item.ParseStatus != filter.ParseStatusPartial || item.Raw == "" {
continue
}
observed := filterufw.ParseRules(item.Rule.Scope, item.Raw)
if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported {
observed[0].Rule.Description = item.Rule.Description
request.Items[index].Rule = observed[0].Rule
request.Items[index].ParseStatus, request.Items[index].Raw = filter.ParseStatusSupported, ""
}
request, err := prepareFirewallRuleCreate(request)
if err != nil {
return dto.FirewallRuleCreateResponse{}, err
}
provider, err := s.selectedProvider(ctx)
@@ -609,30 +624,11 @@ func (s *FirewallService) Create(ctx context.Context, request dto.FirewallRuleCr
if err != nil {
return err
}
return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, false)
return s.runFirewallLifecycleTask(t, client, dto.FirewallLifecycleOperation{Operation: "start"}, nil)
}, nil)
}
ruleState := make(map[string]filter.RuleSet)
taskItem.AddSubTaskWithOps(i18n.GetMsgByKey("FirewallCreateRulesStep"), func(t *task.Task) error {
firewallRuleMutationMu.Lock()
if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil {
firewallRuleMutationMu.Unlock()
return err
}
_, err := s.createRules(t.TaskCtx, request, t, ruleState)
firewallRuleMutationMu.Unlock()
if !request.Initialize || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
if err != nil {
ruleState = nil
}
whitelistErr := s.syncPortWhitelist(t.TaskCtx, ruleState)
if whitelistErr == nil && provider == filter.ProviderFirewalld {
whitelistErr = lifecycleproviders.RemoveFirewalldSSHService()
}
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
return errors.Join(err, whitelistErr)
return s.runFirewallCreateRulesTask(t, request, provider)
}, nil, 0, 0)
if err := taskRepo.Save(context.Background(), taskItem.Task); err != nil {
taskItem.LogFailedWithErr(taskItem.Name, err)
@@ -777,7 +773,11 @@ func (s *FirewallService) restoreFirewallAfterStart(client lifecycle.Client) err
return nil
}
}
recordFailure("restore whitelist allowances", s.SyncPortWhitelist(ctx))
whitelistErr := s.SyncPortWhitelist(ctx)
if whitelistErr == nil && provider == filter.ProviderFirewalld {
whitelistErr = lifecycleproviders.RemoveFirewalldSSHService()
}
recordFailure("restore whitelist allowances", whitelistErr)
return errors.Join(recoveryErrors...)
}
@@ -805,17 +805,9 @@ func (s *FirewallService) restoreFirewalldRuntimeDependents(ctx context.Context,
return restoreErr
}
func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, syncWhitelist bool) error {
func (s *FirewallService) runFirewallLifecycleTask(t *task.Task, client lifecycle.Client, request dto.FirewallLifecycleOperation, prepareStart func(lifecycle.Client) error) error {
ctx := t.TaskCtx
provider := filter.Provider(client.Name())
var prepareStart func(lifecycle.Client) error
if syncWhitelist {
prepareStart = func(lifecycle.Client) error {
return runFirewallLifecycleAction(t, i18n.GetMsgByKey("FirewallSyncWhitelistStep"), func() error {
return s.SyncPortWhitelist(ctx)
})
}
}
operationErr := operateFirewallLifecycle(firewallLifecycleClient{client}, request.Operation, request.WithDockerRestart, prepareStart, t)
if request.Operation == string(lifecycle.OperationStop) {
return operationErr
+117 -3
View File
@@ -554,6 +554,58 @@ func (s *FirewallService) checkSelectedProvider(ctx context.Context, requested f
return nil
}
func prepareFirewallRuleCreate(request dto.FirewallRuleCreate) (dto.FirewallRuleCreate, error) {
if request.BackupFile != "" {
if len(request.Items) != 0 {
return request, filter.ErrInvalidRule
}
items, err := readFirewallBackup(request.BackupFile)
if err != nil {
return request, err
}
for _, item := range items {
request.Items = append(request.Items, dto.FirewallRuleCreateItem{Rule: item.FirewallRule, Raw: item.Raw, ParseStatus: item.ParseStatus, SourceKind: constant.FirewallRuleSourceImported})
}
}
for index, item := range request.Items {
if item.SourceKind != constant.FirewallRuleSourceImported || item.Rule.Scope.Provider != filter.ProviderUFW || item.ParseStatus != filter.ParseStatusPartial || item.Raw == "" {
continue
}
observed := filterufw.ParseRules(item.Rule.Scope, item.Raw)
if len(observed) == 1 && observed[0].ParseStatus == filter.ParseStatusSupported {
observed[0].Rule.Description = item.Rule.Description
request.Items[index].Rule = observed[0].Rule
request.Items[index].ParseStatus, request.Items[index].Raw = filter.ParseStatusSupported, ""
}
}
return request, nil
}
func (s *FirewallService) runFirewallCreateRulesTask(t *task.Task, request dto.FirewallRuleCreate, provider filter.Provider) error {
ruleState := make(map[string]filter.RuleSet)
firewallRuleMutationMu.Lock()
if err := s.checkSelectedProvider(t.TaskCtx, provider); err != nil {
firewallRuleMutationMu.Unlock()
return err
}
_, err := s.createRules(t.TaskCtx, request, t, ruleState)
firewallRuleMutationMu.Unlock()
if !request.Initialize || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
}
if err != nil {
ruleState = nil
}
whitelistErr := s.syncPortWhitelist(t.TaskCtx, ruleState)
if whitelistErr == nil && provider == filter.ProviderFirewalld {
whitelistErr = lifecycleproviders.RemoveFirewalldSSHService()
}
t.LogWithStatus(i18n.GetMsgByKey("FirewallSyncWhitelistStep"), whitelistErr)
return errors.Join(err, whitelistErr)
}
func (s *FirewallService) createRules(ctx context.Context, request dto.FirewallRuleCreate, t *task.Task, ruleState map[string]filter.RuleSet) (result dto.FirewallRuleCreateResponse, taskErr error) {
var firstFailure error
defer func() {
@@ -1734,9 +1786,6 @@ func operateFirewallLifecycle(client lifecycle.Client, operation string, withDoc
var recoveryErrors []error
if prepareStart != nil {
err := prepareStart(client)
if err == nil && client.Name() == constant.FirewallProviderFirewalld {
err = lifecycleproviders.RemoveFirewalldSSHService()
}
if err != nil {
recoveryErrors = append(recoveryErrors, fmt.Errorf("prepare firewall after %s: %w", operation, err))
}
@@ -1758,17 +1807,82 @@ func operateFirewallLifecycle(client lifecycle.Client, operation string, withDoc
}
func (c firewallLifecycleClient) Start() error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := c.prepareAccessWhitelistBeforeStart(); err != nil {
global.LOG.Warnf("prepare %s access whitelist before start failed: %v", c.Name(), err)
}
return c.Client.Start()
}
func (c firewallLifecycleClient) Restart() error {
firewallWhitelistMu.Lock()
defer firewallWhitelistMu.Unlock()
firewallRuleMutationMu.Lock()
defer firewallRuleMutationMu.Unlock()
if err := c.prepareAccessWhitelistBeforeStart(); err != nil {
global.LOG.Warnf("prepare %s access whitelist before restart failed: %v", c.Name(), err)
}
return c.Client.Restart()
}
func (c firewallLifecycleClient) prepareAccessWhitelistBeforeStart() error {
provider := filter.Provider(c.Name())
if provider != filter.ProviderFirewalld && provider != filter.ProviderUFW {
return nil
}
ports, err := LoadRequiredFirewallPortWhiteList()
if err != nil {
return err
}
var adapter filter.Adapter
var active bool
if provider == filter.ProviderFirewalld {
active, err = c.Status()
if err != nil {
return err
}
adapter = filterfirewalld.NewAdapter()
} else {
adapter = filterufw.NewAdapter()
}
var commands []filter.NativeCommand
for _, port := range firewall.ExpandPortWhitelist(ports) {
rule := firewall.RuleForSystemPort(provider, port)
rule.Scope = rule.Scope.Normalize()
rule.UUID = uuid.NewString()
if provider == filter.ProviderUFW {
position := int64(1)
rule.OrderIndex = &position
}
plan, err := adapter.BuildCommands(filter.RuleSet{Scope: rule.Scope}, []filter.RuleChange{{Operation: filter.ChangeCreate, After: &rule}})
if err != nil {
return err
}
for _, item := range plan.Rules {
for _, command := range item.Commands {
if provider == filter.ProviderFirewalld {
if active {
command.Args = append(command.Args, "--permanent")
} else {
command.Executable = "firewall-offline-cmd"
}
}
commands = append(commands, command)
}
}
}
manager := cmd.NewCommandMgr(cmd.WithTimeout(60*time.Second), cmd.WithEnv("LC_ALL=C", "LANGUAGE=en_US:en"))
for _, command := range commands {
if err := manager.RunWithOptionalSudo(command.Executable, command.Args...); err != nil {
return fmt.Errorf("prepare %s access whitelist before start: %w", provider, err)
}
}
return nil
}
func runFirewallLifecycleAction(t *task.Task, name string, action func() error) error {
t.Log(i18n.GetWithName("TaskStart", name))
started := time.Now()
+1 -1
View File
@@ -55,7 +55,7 @@ type IMonitorService interface {
LoadIOOptions() []string
LoadNetworkOptions() []string
LoadVLLMMonitorData(ctx context.Context, req dto.MonitorVLLMSearch) (dto.MonitorVLLMData, error)
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error)
LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (dto.MonitorVLLMCurrentData, error)
CleanVLLMMonitor(req dto.MonitorVLLMClean) error
LoadGPUOptions() dto.MonitorGPUOptions
LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error)
+34 -28
View File
@@ -60,6 +60,9 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
if err != nil {
return data, err
}
for i := range points {
points[i].MonitorVLLM.CompletedRequests = points[i].CompletedRequests
}
if data.BucketSeconds > 0 {
aggregates := map[int64]*vllm.HistoryAccumulator{}
err = vllmMonitorRepo.WalkDeltas(ctx, req.AppInstallID, req.StartTime, req.EndTime, data.BucketSeconds, req.Aggregation, req.Group, func(bucket int64, histograms, requests string) error {
@@ -91,15 +94,6 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
data.Points = append(data.Points, point)
}
} else {
setting, err := settingRepo.GetValueByKey("VLLMMonitorInterval")
if err != nil {
return data, err
}
intervalSeconds, err := strconv.Atoi(setting)
if err != nil || intervalSeconds <= 0 {
return data, fmt.Errorf("invalid vLLM monitoring interval: %s", setting)
}
interval := time.Duration(intervalSeconds) * time.Second
for i, point := range points {
if err := ctx.Err(); err != nil {
return data, err
@@ -109,8 +103,20 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
return data, err
}
aggregate.Apply(&point.MonitorVLLM, "max")
if i > 0 && point.CreatedAt.Sub(points[i-1].CreatedAt) > 2*interval {
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: points[i-1].CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
if i > 0 {
previous := points[i-1]
interval := time.Duration(max(previous.SampleIntervalSeconds, point.SampleIntervalSeconds)) * time.Second
if interval == 0 {
if i > 1 {
interval = previous.CreatedAt.Sub(points[i-2].CreatedAt)
}
if i+1 < len(points) {
interval = max(interval, points[i+1].CreatedAt.Sub(point.CreatedAt))
}
}
if interval > 0 && point.CreatedAt.Sub(previous.CreatedAt) > 2*interval {
data.Points = append(data.Points, model.MonitorVLLM{CreatedAt: previous.CreatedAt.Add(interval), AppInstallID: req.AppInstallID})
}
}
data.Points = append(data.Points, point.MonitorVLLM)
}
@@ -118,31 +124,30 @@ func (m *MonitorService) LoadVLLMMonitorData(ctx context.Context, req dto.Monito
return data, nil
}
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (model.MonitorVLLM, error) {
func (m *MonitorService) LoadVLLMCurrent(ctx context.Context, req dto.MonitorVLLMCurrent) (dto.MonitorVLLMCurrentData, error) {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.AppInstallID))
if err != nil {
return model.MonitorVLLM{}, err
return dto.MonitorVLLMCurrentData{}, err
}
if install.App.Key != "vllm" {
return model.MonitorVLLM{}, fmt.Errorf("not a vLLM installation")
return dto.MonitorVLLMCurrentData{}, fmt.Errorf("not a vLLM installation")
}
previous, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
point, err := collectVLLMMetrics(ctx, install, model.MonitorVLLM{})
data := dto.MonitorVLLMCurrentData{MonitorVLLM: point}
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
return previous, nil
return data, nil
}
timer := time.NewTimer(time.Second)
defer timer.Stop()
select {
case <-ctx.Done():
return model.MonitorVLLM{}, ctx.Err()
case <-timer.C:
if err := json.Unmarshal([]byte(point.RawMetrics), &data.Metrics); err != nil {
return data, err
}
point, err := collectVLLMMetrics(ctx, install, previous)
if err != nil {
global.LOG.Debugf("Collect vLLM metrics on port %d failed: %v", install.HttpPort, err)
for name := range data.Metrics {
if name == "request_prompt_tokens_sum" || name == "request_prompt_tokens_count" ||
(!strings.HasSuffix(name, "_total") && !strings.HasSuffix(name, "_sum") && !strings.HasSuffix(name, "_count")) {
delete(data.Metrics, name)
}
}
return point, nil
return data, nil
}
func (m *MonitorService) CleanVLLMMonitor(req dto.MonitorVLLMClean) error {
@@ -164,7 +169,7 @@ func StartVLLMMonitor(interval string) error {
return fmt.Errorf("invalid vLLM monitoring interval: %s", interval)
}
service := &MonitorService{}
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(service.saveVLLMData))
job := cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(cron.FuncJob(func() { service.saveVLLMData(seconds) }))
id, err := global.Cron.AddFunc(fmt.Sprintf("@every %ds", seconds), func() { go job.Run() })
if err != nil {
return err
@@ -176,7 +181,7 @@ func StartVLLMMonitor(interval string) error {
return nil
}
func (m *MonitorService) saveVLLMData() {
func (m *MonitorService) saveVLLMData(intervalSeconds int) {
if !vllmMonitorMutex.TryLock() {
return
}
@@ -228,6 +233,7 @@ func (m *MonitorService) saveVLLMData() {
return err
}
point, err := collectVLLMMetrics(context.Background(), install, previous)
point.SampleIntervalSeconds = intervalSeconds
if err != nil {
global.LOG.Debugf("Collect vLLM metrics for %s failed: %v", install.Name, err)
}
+1
View File
@@ -114,6 +114,7 @@ func agentDBMigrations() []*gormigrate.Migration {
migrations.AddVLLMMonitor,
migrations.AddMonitorSettings,
migrations.AddVLLMRequestMetrics,
migrations.AddVLLMSampleInterval,
}
}
+7
View File
@@ -1881,3 +1881,10 @@ var AddVLLMRequestMetrics = &gormigrate.Migration{
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
var AddVLLMSampleInterval = &gormigrate.Migration{
ID: "20261016-vllm-sample-interval",
Migrate: func(tx *gorm.DB) error {
return global.VLLMMonitorDB.AutoMigrate(&model.MonitorVLLM{})
},
}
+11 -1
View File
@@ -314,7 +314,17 @@ func (s *Session) detach(a *attachment, clean, revalidate bool, cursor uint64) {
if !s.Persistent || revalidate {
timeout = revalidateGrace
}
s.grace = time.AfterFunc(timeout, s.Close)
// Stop cannot cancel a fired timer; a racing Attach clears s.grace.
var grace *time.Timer
grace = time.AfterFunc(timeout, func() {
s.mu.Lock()
expired := s.grace == grace
s.mu.Unlock()
if expired {
s.Close()
}
})
s.grace = grace
}
s.mu.Unlock()
if clean {
-1
View File
@@ -266,7 +266,6 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) {
Language: settingInfo.Language,
MenuTabs: settingInfo.MenuTabs,
MenuAccordion: settingInfo.MenuAccordion,
PanelCache: settingInfo.PanelCache,
PanelName: settingInfo.PanelName,
Theme: settingInfo.Theme,
NeedCaptcha: needCaptcha,
-13
View File
@@ -4,16 +4,6 @@ import (
"time"
)
const DefaultPanelCache = `{"tabs":true,"pagination":true,"refresh":true,"search":false,"duration":"24h"}`
type PanelCache struct {
Tabs *bool `json:"tabs"`
Pagination *bool `json:"pagination"`
Refresh *bool `json:"refresh"`
Search *bool `json:"search"`
Duration string `json:"duration"`
}
type SettingInfo struct {
SystemVersion string `json:"systemVersion"`
DeveloperMode string `json:"developerMode"`
@@ -28,7 +18,6 @@ type SettingInfo struct {
Theme string `json:"theme"`
MenuTabs string `json:"menuTabs"`
MenuAccordion string `json:"menuAccordion"`
PanelCache string `json:"panelCache"`
Language string `json:"language"`
DocSource string `json:"docSource"`
@@ -73,7 +62,6 @@ type SettingBaseInfo struct {
Theme string `json:"theme"`
MenuTabs string `json:"menuTabs"`
MenuAccordion string `json:"menuAccordion"`
PanelCache string `json:"panelCache"`
Language string `json:"language"`
HideMenu string `json:"hideMenu"`
DocSource string `json:"docSource"`
@@ -300,7 +288,6 @@ type LoginSetting struct {
Language string `json:"language"`
MenuTabs string `json:"menuTabs"`
MenuAccordion string `json:"menuAccordion"`
PanelCache string `json:"panelCache"`
PanelName string `json:"panelName"`
Theme string `json:"theme"`
NeedCaptcha bool `json:"needCaptcha"`
-27
View File
@@ -120,9 +120,6 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
for _, set := range setting {
settingMap[set.Key] = set.Value
}
if settingMap["PanelCache"] == "" {
settingMap["PanelCache"] = dto.DefaultPanelCache
}
repairAndSortHideMenu(settingMap)
var info dto.SettingInfo
arr, err := json.Marshal(settingMap)
@@ -158,9 +155,6 @@ func (u *SettingService) GetSettingBaseInfo() (*dto.SettingBaseInfo, error) {
for _, set := range setting {
settingMap[set.Key] = set.Value
}
if settingMap["PanelCache"] == "" {
settingMap["PanelCache"] = dto.DefaultPanelCache
}
repairAndSortHideMenu(settingMap)
var info dto.SettingBaseInfo
arr, err := json.Marshal(settingMap)
@@ -230,27 +224,6 @@ func (u *SettingService) Update(c *gin.Context, key, value string) error {
if auth.IsAPICredentialSetting(key) {
return buserr.New("ErrInvalidParams")
}
if key == "PanelCache" {
var config dto.PanelCache
decoder := json.NewDecoder(strings.NewReader(value))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&config); err != nil {
return buserr.New("ErrInvalidParams")
}
if err := decoder.Decode(new(any)); err != io.EOF {
return buserr.New("ErrInvalidParams")
}
if config.Tabs == nil || config.Pagination == nil || config.Refresh == nil || config.Search == nil {
return buserr.New("ErrInvalidParams")
}
switch config.Duration {
case "24h", "week", "month", "forever":
default:
return buserr.New("ErrInvalidParams")
}
data, _ := json.Marshal(config)
return settingRepo.UpdateOrCreate(key, string(data))
}
oldVal, err := settingRepo.Get(repo.WithByKey(key))
if err != nil {
return err
-1
View File
@@ -26,7 +26,6 @@ func Init() {
}
var baseSettingKeys = map[string]struct{}{
"PanelCache": {},
"PanelName": {},
"Theme": {},
"MenuTabs": {},
-1
View File
@@ -77,7 +77,6 @@ export namespace Login {
language: string;
menuTabs: string;
menuAccordion: string;
panelCache: string;
panelName: string;
theme: string;
isOffline: boolean;
-2
View File
@@ -41,7 +41,6 @@ export namespace Setting {
theme: string;
menuTabs: string;
menuAccordion: string;
panelCache: string;
language: string;
docSource: string;
@@ -87,7 +86,6 @@ export namespace Setting {
theme: string;
menuTabs: string;
menuAccordion: string;
panelCache: string;
language: string;
hideMenu: string;
docSource: string;
+4 -2
View File
@@ -1,4 +1,5 @@
import http from '@/api';
import type { AxiosRequestConfig } from 'axios';
import { ResPage, SearchWithPage } from '../interface';
import { Container } from '../interface/container';
import { TimeoutEnum } from '@/enums/http-enum';
@@ -23,10 +24,11 @@ export const loadContainerUsers = (name: string) => {
export const listContainerFiles = (params: Container.ContainerFileReq) => {
return http.post<Array<Container.ContainerFileInfo>>(`/containers/files/search`, params, TimeoutEnum.T_40S);
};
export const uploadContainerFile = (params: FormData) => {
export const uploadContainerFile = (params: FormData, config?: AxiosRequestConfig & { skipErrorMessage?: boolean }) => {
return http.upload(`/containers/files/upload`, params, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: TimeoutEnum.T_5M,
...config,
headers: { 'Content-Type': 'multipart/form-data', ...config?.headers },
});
};
export const getContainerFileContent = (params: Container.ContainerFileReq) => {
+2 -2
View File
@@ -21,10 +21,10 @@ export const loadForwardBaseInfo = () =>
export const searchForwardRule = (request: Firewall.ForwardRuleSearch) =>
http.post<ResPage<Firewall.RuleInfo>>('/hosts/firewall/forward/search', request, TimeoutEnum.T_40S);
export const operateFire = (operation: string, withDockerRestart: boolean) =>
export const operateFire = (operation: string, withDockerRestart: boolean, backupFile?: string) =>
http.post<Firewall.FilterChainOperationResult>(
'/hosts/firewall/operate',
{ operation, withDockerRestart },
{ operation, withDockerRestart, backupFile },
TimeoutEnum.T_10M,
);
+45 -43
View File
@@ -20,55 +20,57 @@
</template>
<script setup lang="ts">
import { onActivated, onDeactivated, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { panelCacheKey, readPanelCache, writePanelCache } from '@/utils/panel-cache';
import { onMounted, onUnmounted, ref } from 'vue';
defineOptions({ name: 'TableSetting' });
const refreshRateUnit = ref(0);
const refreshRateUnit = ref<number>(0);
const emit = defineEmits(['search']);
const props = defineProps({ title: String, rate: Number });
const route = useRoute();
// Capture the owning page; useRoute continues to change while a page is deactivated.
const pagePath = route.path;
const cacheKey = () => panelCacheKey(`refresh:${pagePath}:${props.title || ''}`);
let timer: ReturnType<typeof setInterval> | undefined;
let active = false;
const stopTimer = () => {
clearInterval(timer);
timer = undefined;
};
const startTimer = () => {
stopTimer();
if (active && refreshRateUnit.value > 0) {
timer = setInterval(() => emit('search'), 1000 * refreshRateUnit.value);
const props = defineProps({
title: String,
rate: Number,
});
let timer: NodeJS.Timer | null = null;
const changeRefresh = (command: number) => {
refreshRateUnit.value = command || 0;
if (refreshRateUnit.value !== 0) {
if (timer) {
clearInterval(Number(timer));
timer = null;
}
timer = setInterval(() => {
emit('search');
}, 1000 * refreshRateUnit.value);
} else {
if (timer) {
clearInterval(Number(timer));
timer = null;
}
}
localStorage.setItem(props.title, refreshRateUnit.value + '');
};
const restore = () => {
const rate = readPanelCache<number>('refresh', cacheKey());
refreshRateUnit.value = Number.isFinite(rate) && rate >= 0 ? rate : props.rate || 0;
startTimer();
};
const changeRefresh = (rate: number) => {
refreshRateUnit.value = rate;
writePanelCache('refresh', cacheKey(), rate);
startTimer();
};
const activate = () => {
if (active) return;
active = true;
restore();
};
watch(cacheKey, () => {
if (active) restore();
onUnmounted(() => {
clearInterval(Number(timer));
timer = null;
if (props.title) {
localStorage.setItem(props.title, refreshRateUnit.value + '');
}
});
onMounted(activate);
onActivated(activate);
onDeactivated(() => {
active = false;
stopTimer();
onMounted(() => {
if (props.title && localStorage.getItem(props.title) != null) {
let rate = Number(localStorage.getItem(props.title));
refreshRateUnit.value = rate ? Number(rate) : 0;
changeRefresh(refreshRateUnit.value);
return;
}
if (props.rate) {
refreshRateUnit.value = props.rate;
changeRefresh(refreshRateUnit.value);
}
});
onUnmounted(stopTimer);
</script>
<style lang="scss" scoped>
@@ -167,7 +167,6 @@
</div>
</template>
<script setup lang="ts">
import { panelCacheKey, writePanelCache } from '@/utils/panel-cache';
import { computed, onDeactivated, onMounted, ref, useAttrs } from 'vue';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { flattenVNodes, type FuTableOperationButton } from '@/components/table/shared';
@@ -342,33 +341,13 @@ const handleRightClick = (row, column, event) => {
openContextMenu(row, event);
};
watch(
() => props.data,
() => {
const pagination = props.paginationConfig;
if (!pagination || !pagination.pageSize) return;
const lastPage = Math.max(1, Math.ceil(pagination.total / pagination.pageSize));
if (pagination.currentPage > lastPage) {
pagination.currentPage = lastPage;
emit('search');
}
},
{ flush: 'post' },
);
function currentChange() {
emit('search');
}
function sizeChange() {
if (props.paginationConfig.cacheSizeKey) {
writePanelCache(
'pagination',
panelCacheKey(`size:${props.paginationConfig.cacheSizeKey}`),
props.paginationConfig.pageSize,
);
}
props.paginationConfig.currentPage = 1;
localStorage.setItem(props.paginationConfig.cacheSizeKey, props.paginationConfig.pageSize);
emit('search');
}
+1 -2
View File
@@ -52,7 +52,6 @@
</template>
<script setup lang="ts">
import { readCachedPageSize } from '@/utils/panel-cache';
import TaskLog from '@/components/log/task/index.vue';
import NodeSelect from '@/components/node-select/index.vue';
@@ -74,7 +73,7 @@ const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'task-list-page-size',
currentPage: 1,
pageSize: readCachedPageSize('task-list-page-size') || 20,
pageSize: Number(localStorage.getItem('task-list-page-size')) || 20,
total: 0,
small: true,
});
@@ -18,6 +18,7 @@
size="w-70"
:show-close="false"
:modal="false"
modal-penetrable
@opened="claim"
>
<template #header>
@@ -7,6 +7,7 @@
:ref="(el: any) => store.setInstance(item.key, el)"
@session="(id: string) => store.setSessionId(item.key, id)"
@expired="store.onExpired(item.key)"
@reconnect="store.reconnect(item.key)"
/>
</Teleport>
</template>
+11 -1
View File
@@ -31,7 +31,7 @@ const { currentNode } = useGlobalStore();
// session: agent side session id known (fresh or reattached)
// expired: the agent no longer has the session; a reconnect must open a new one
const emit = defineEmits(['session', 'expired']);
const emit = defineEmits(['session', 'expired', 'reconnect']);
// Close codes of the agent's session protocol (agent/utils/terminal/session.go).
const CLOSE_SESSION_NOT_FOUND = 4404;
@@ -56,6 +56,7 @@ let closing = false;
let reconnecting = false;
let reconnectNoticeShown = false;
let revalidating = false;
let expired = false;
let reconnectStartedAt = 0;
let reconnectDelay = 1000;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
@@ -279,6 +280,7 @@ function resizeTerminal() {
if (terminalElement.value.clientWidth <= 0 || terminalElement.value.clientHeight <= 0) {
return;
}
if (terminalElement.value.closest('.terminal-host')) return;
fitAddon.fit();
if (isWsOpen()) {
@@ -484,6 +486,7 @@ const closeRealTerminal = (ev: CloseEvent) => {
'31',
ev.code === 1000 ? 'The connection has been disconnected.' : i18n.global.t('terminal.sessionExpired'),
);
expired = true;
emit('expired');
return;
case CLOSE_ATTACHED_ELSEWHERE:
@@ -602,6 +605,13 @@ function sendMsg(data: string, line: string = '') {
function onTermData(data: string) {
if (!data) return;
if (expired) {
if (isEnterInputData(data)) {
expired = false;
emit('reconnect');
}
return;
}
if (aiNotice.value.loading) return;
sendMsg(data, isEnterInputData(data) ? getCurrentTerminalLine() : '');
}
@@ -0,0 +1,24 @@
import { storeToRefs } from 'pinia';
import { onActivated, onDeactivated } from 'vue';
import GlobalStore from '@/store/modules/global';
import { bindSearchReset, type SearchFields } from '@/utils/search-reset';
interface SearchResetOptions {
scope?: () => string | number | undefined;
onRevisit: () => unknown;
}
export const useSearchReset = (fields: SearchFields, options: SearchResetOptions) => {
const { currentNode } = storeToRefs(GlobalStore());
const reset = bindSearchReset(options.scope ? [currentNode, options.scope] : [currentNode], fields);
let deactivated = false;
onDeactivated(() => {
deactivated = true;
});
onActivated(() => {
if (!deactivated) return;
deactivated = false;
reset();
return options.onRevisit();
});
};
+14 -15
View File
@@ -1517,6 +1517,14 @@ const message = {
extraHeaders: 'Extra Headers',
},
vllmMonitor: {
concurrency: 'Current concurrency',
sampleMoment: 'At sampling time',
sampleWindow: 'Sample interval',
requestUnit: 'requests',
processing: 'Processing',
queued: 'Queued',
requestTiming: 'Request timing',
completedRequests: 'Requests completed in interval',
tokensPerSecond: 'tokens/s',
requestsPerSecond: 'Requests/s',
finishReasonLabels: {
@@ -2962,20 +2970,6 @@ const message = {
remote: 'Remote SSH',
},
setting: {
panelCache: {
title: 'Cache settings',
tabs: 'Tab cache',
pagination: 'Pagination cache',
refresh: 'Refresh interval cache',
search: 'Search input cache',
duration: 'Cache duration',
'24h': '24h',
week: 'One week',
month: 'One month',
forever: 'Forever',
helper: 'Expires after the last state change. Expired values return to page defaults.',
},
panel: 'Panel',
user: 'Panel user',
userChange: 'Change panel user',
@@ -4313,9 +4307,14 @@ const message = {
familyIncomplete: '{0} chains are incomplete',
resetWithBackup: 'Back up rules and reset',
resetOnly: 'Reset only',
resetWithBackupHelper: 'Save the current module rules as JSON for restoration during initialization.',
resetWithBackupHelper:
'Save the current module rules as JSON for restoration during initialization or startup.',
resetOnlyHelper: 'Clear current rules without creating a new backup. Existing backups are kept.',
backupDirectoryHelper: 'Valid rule backups in {0} are listed below. The newest file is selected by default.',
startOnly: 'Start only',
startAndRestore: 'Start and restore from backup',
startWithBackupHelper:
'Start the firewall normally, or choose an existing JSON backup to start and restore rules.',
restoreAllRules: 'Initialize and restore rules',
importRuleBackup: 'Import from backup',
initializeOnly: 'Initialize only',
+13 -15
View File
@@ -1526,6 +1526,14 @@ const message = {
extraHeaders: 'Encabezados adicionales',
},
vllmMonitor: {
concurrency: 'Concurrencia actual',
sampleMoment: 'Al tomar la muestra',
sampleWindow: 'Intervalo de muestreo',
requestUnit: 'solicitudes',
processing: 'En proceso',
queued: 'En cola',
requestTiming: 'Tiempo de las solicitudes',
completedRequests: 'Solicitudes completadas en el intervalo',
tokensPerSecond: 'tokens/s',
requestsPerSecond: 'Solicitudes/s',
finishReasonLabels: {
@@ -3007,20 +3015,6 @@ const message = {
remote: 'SSH remoto',
},
setting: {
panelCache: {
title: 'Configuración de caché',
tabs: 'Caché de pestañas',
pagination: 'Caché de paginación',
refresh: 'Caché del intervalo de actualización',
search: 'Caché del campo de búsqueda',
duration: 'Duración de la caché',
'24h': '24 horas',
week: 'Una semana',
month: 'Un mes',
forever: 'Permanente',
helper: 'El plazo se calcula desde el último cambio de estado. Al caducar, se restablecen los valores predeterminados.',
},
panel: 'Panel',
user: 'Usuario del panel',
userChange: 'Cambiar usuario del panel',
@@ -4361,10 +4355,14 @@ const message = {
resetWithBackup: 'Guardar reglas y restablecer',
resetOnly: 'Solo restablecer',
resetWithBackupHelper:
'Guarda las reglas del módulo actual en JSON para restaurarlas durante la inicialización.',
'Guarde las reglas del módulo actual en JSON para restaurarlas al inicializar o iniciar el cortafuegos.',
resetOnlyHelper: 'Borrar las reglas actuales sin crear una copia nueva. Se conservan las copias existentes.',
backupDirectoryHelper:
'Se muestran las copias válidas de reglas en {0}. Se selecciona la más reciente por defecto.',
startOnly: 'Solo iniciar',
startAndRestore: 'Iniciar y restaurar desde una copia de seguridad',
startWithBackupHelper:
'Inicie el cortafuegos normalmente o elija una copia JSON existente para iniciar y restaurar reglas.',
restoreAllRules: 'Inicializar y restaurar reglas',
importRuleBackup: 'Importar desde copia de seguridad',
initializeOnly: 'Solo inicializar',
+14 -15
View File
@@ -1502,6 +1502,14 @@ const message = {
extraHeaders: 'هدرهای اضافی',
},
vllmMonitor: {
concurrency: 'درخواست‌های هم‌زمان فعلی',
sampleMoment: 'در لحظه نمونه‌برداری',
sampleWindow: 'بازه نمونه‌برداری',
requestUnit: 'درخواست',
processing: 'در حال پردازش',
queued: 'در صف',
requestTiming: 'زمان پردازش درخواست',
completedRequests: 'تعداد درخواست‌های تکمیل‌شده در بازه',
tokensPerSecond: 'tokens/ثانیه',
requestsPerSecond: 'درخواست/ثانیه',
finishReasonLabels: {
@@ -2932,20 +2940,6 @@ const message = {
remote: 'SSH از راه دور',
},
setting: {
panelCache: {
title: 'تنظیمات حافظهٔ نهان',
tabs: 'حافظهٔ نهان زبانه‌ها',
pagination: 'حافظهٔ نهان صفحه‌بندی',
refresh: 'حافظهٔ نهان فاصلهٔ بازخوانی',
search: 'حافظهٔ نهان ورودی جستجو',
duration: 'مدت نگهداری حافظهٔ نهان',
'24h': '۲۴ ساعت',
week: 'یک هفته',
month: 'یک ماه',
forever: 'دائمی',
helper: 'مدت اعتبار از آخرین تغییر وضعیت محاسبه می‌شود. پس از انقضا، مقادیر پیش‌فرض بازیابی می‌شوند.',
},
panel: 'پنل',
user: 'کاربر پنل',
userChange: 'تغییر کاربر پنل',
@@ -4273,10 +4267,15 @@ const message = {
familyIncomplete: 'زنجیره‌های {0} ناقص هستند',
resetWithBackup: 'پشتیبان‌گیری قوانین و بازنشانی',
resetOnly: 'فقط بازنشانی',
resetWithBackupHelper: 'قوانین ماژول فعلی را به‌صورت JSON ذخیره کنید تا هنگام مقداردهی اولیه بازیابی شوند.',
resetWithBackupHelper:
'قوانین ماژول فعلی را به‌صورت JSON ذخیره کنید تا هنگام مقداردهی اولیه یا راه‌اندازی بازیابی شوند.',
resetOnlyHelper: 'قوانین فعلی بدون ایجاد نسخه پشتیبان جدید پاک می‌شوند. نسخه‌های پشتیبان قبلی حفظ می‌شوند.',
backupDirectoryHelper:
'نسخه‌های پشتیبان معتبر قوانین در {0} نمایش داده می‌شوند. جدیدترین فایل به‌طور پیش‌فرض انتخاب می‌شود.',
startOnly: 'فقط راه\u200cاندازی',
startAndRestore: 'راه\u200cاندازی و بازیابی از نسخه پشتیبان',
startWithBackupHelper:
'فایروال را به‌طور معمول راه‌اندازی کنید یا یک نسخه پشتیبان JSON موجود را برای راه‌اندازی و بازیابی قوانین انتخاب کنید.',
restoreAllRules: 'مقداردهی اولیه و بازیابی قوانین',
importRuleBackup: 'ورود از نسخه پشتیبان',
initializeOnly: 'فقط راه‌اندازی',
+12 -15
View File
@@ -1505,6 +1505,14 @@ const message = {
extraHeaders: '追加ヘッダー',
},
vllmMonitor: {
concurrency: '現在の同時リクエスト',
sampleMoment: 'サンプリング時点',
sampleWindow: '集計区間',
requestUnit: '件',
processing: '処理中',
queued: '待機中',
requestTiming: 'リクエスト処理時間',
completedRequests: '区間内の完了リクエスト数',
tokensPerSecond: 'tokens/秒',
requestsPerSecond: 'リクエスト/秒',
finishReasonLabels: {
@@ -2937,20 +2945,6 @@ const message = {
remote: 'リモートSSH',
},
setting: {
panelCache: {
title: 'キャッシュ設定',
tabs: 'タブのキャッシュ',
pagination: 'ページネーションのキャッシュ',
refresh: '更新間隔のキャッシュ',
search: '検索入力のキャッシュ',
duration: 'キャッシュ期間',
'24h': '24時間',
week: '1週間',
month: '1か月',
forever: '無期限',
helper: '有効期間は状態を最後に変更した時点から計算されます。期限が切れると既定値に戻ります。',
},
panel: 'パネル',
user: 'パネルユーザー',
userChange: 'パネルユーザーを変更します',
@@ -4296,9 +4290,12 @@ const message = {
familyIncomplete: '{0} のチェーンが不完全です',
resetWithBackup: 'ルールをバックアップしてリセット',
resetOnly: 'リセットのみ',
resetWithBackupHelper: '現在のモジュールのルールを JSON に保存し、初期化時に復元できます。',
resetWithBackupHelper: '現在のモジュールのルールを JSON に保存し、初期化時または起動時に復元できます。',
resetOnlyHelper: '新しいバックアップを作成せずに現在のルールを消去します。既存のバックアップは保持されます。',
backupDirectoryHelper: '{0} 内の有効なルールバックアップを表示します。既定では最新のファイルが選択されます。',
startOnly: '起動のみ',
startAndRestore: '起動してバックアップから復元',
startWithBackupHelper: '通常起動するか、既存の JSON バックアップを選んで起動とルールの復元を行います。',
restoreAllRules: '初期化してルールを復元',
importRuleBackup: 'バックアップからインポート',
initializeOnly: '初期化のみ',
+12 -15
View File
@@ -1490,6 +1490,14 @@ const message = {
extraHeaders: '추가 헤더',
},
vllmMonitor: {
concurrency: '현재 동시 요청',
sampleMoment: '샘플링 시점',
sampleWindow: '집계 구간',
requestUnit: '개 요청',
processing: '처리 중',
queued: '대기 중',
requestTiming: '요청 소요 시간',
completedRequests: '구간 내 완료 요청 수',
tokensPerSecond: 'tokens/초',
requestsPerSecond: '요청/초',
finishReasonLabels: {
@@ -2898,20 +2906,6 @@ const message = {
remote: '원격 SSH',
},
setting: {
panelCache: {
title: '캐시 설정',
tabs: '탭 캐시',
pagination: '페이지 이동 캐시',
refresh: '새로 고침 간격 캐시',
search: '검색 입력 캐시',
duration: '캐시 유지 기간',
'24h': '24시간',
week: '1주',
month: '1개월',
forever: '무기한',
helper: '유효 기간은 상태를 마지막으로 변경한 시점부터 계산됩니다. 만료되면 기본값으로 돌아갑니다.',
},
panel: '패널',
user: '패널 사용자',
userChange: '패널 사용자 변경',
@@ -4218,9 +4212,12 @@ const message = {
familyIncomplete: '{0} 체인이 불완전합니다',
resetWithBackup: '규칙 백업 후 초기화',
resetOnly: '초기화만 수행',
resetWithBackupHelper: '현재 모듈의 규칙을 JSON으로 저장하여 초기화 시 복원할 수 있습니다.',
resetWithBackupHelper: '현재 모듈의 규칙을 JSON으로 저장하여 초기화 또는 시작 시 복원할 수 있습니다.',
resetOnlyHelper: '새 백업 없이 현재 규칙을 삭제합니다. 기존 백업은 유지됩니다.',
backupDirectoryHelper: '{0}의 유효한 규칙 백업을 표시합니다. 기본적으로 최신 파일이 선택됩니다.',
startOnly: '시작만',
startAndRestore: '시작 및 백업에서 복원',
startWithBackupHelper: '방화벽을 일반적으로 시작하거나 기존 JSON 백업을 선택하여 시작하고 규칙을 복원합니다.',
restoreAllRules: '초기화 및 규칙 복원',
importRuleBackup: '백업에서 가져오기',
initializeOnly: '초기화만 수행',
+12 -15
View File
@@ -1484,6 +1484,14 @@ const message = {
extraHeaders: 'Headers ເພີ່ມເຕີມ',
},
vllmMonitor: {
concurrency: 'ຄຳຮ້ອງຂໍພ້ອມກັນໃນປັດຈຸບັນ',
sampleMoment: 'ໃນເວລາເກັບຕົວຢ່າງ',
sampleWindow: 'ຊ່ວງເວລາເກັບຕົວຢ່າງ',
requestUnit: 'ຄຳຮ້ອງຂໍ',
processing: 'ກຳລັງປະມວນຜົນ',
queued: 'ກຳລັງລໍຖ້າ',
requestTiming: 'ເວລາປະມວນຜົນຄຳຮ້ອງຂໍ',
completedRequests: 'ຈຳນວນຄຳຮ້ອງຂໍທີ່ສຳເລັດໃນຊ່ວງເວລາ',
tokensPerSecond: 'tokens/ວິນາທີ',
requestsPerSecond: 'ຄຳຮ້ອງຂໍ/ວິນາທີ',
finishReasonLabels: {
@@ -2879,20 +2887,6 @@ const message = {
remote: 'SSH ທາງໄກ',
},
setting: {
panelCache: {
title: 'ການຕັ້ງຄ່າແຄດ',
tabs: 'ແຄດແຖບ',
pagination: 'ແຄດການແບ່ງໜ້າ',
refresh: 'ແຄດຊ່ວງເວລາຣີເຟຣຊ',
search: 'ແຄດຂໍ້ຄວາມຄົ້ນຫາ',
duration: 'ໄລຍະເວລາເກັບແຄດ',
'24h': '24 ຊົ່ວໂມງ',
week: 'ໜຶ່ງອາທິດ',
month: 'ໜຶ່ງເດືອນ',
forever: 'ຖາວອນ',
helper: 'ໄລຍະເວລາຈະນັບຈາກການປ່ຽນສະຖານະຄັ້ງຫຼ້າສຸດ. ເມື່ອໝົດອາຍຸ ຈະກັບຄືນເປັນຄ່າເລີ່ມຕົ້ນ.',
},
panel: 'ພາເນລ',
user: 'ຜູ້ໃຊ້ພາເນລ',
userChange: 'ປ່ຽນຜູ້ໃຊ້ພາເນລ',
@@ -4187,9 +4181,12 @@ const message = {
familyIncomplete: 'ຊຸດກົດ {0} ບໍ່ຄົບຖ້ວນ',
resetWithBackup: 'ສຳຮອງກົດແລ້ວຣີເຊັດ',
resetOnly: 'ຣີເຊັດເທົ່ານັ້ນ',
resetWithBackupHelper: 'ບັນທຶກກົດຂອງໂມດູນປັດຈຸບັນເປັນ JSON ເພື່ອກູ້ຄືນເມື່ອເລີ່ມຕົ້ນ.',
resetWithBackupHelper: 'ບັນທຶກກົດຂອງໂມດູນປັດຈຸບັນເປັນ JSON ເພື່ອກູ້ຄືນເມື່ອເລີ່ມຕົ້ນ ຫຼື ເປີດໃຊ້ໄຟວໍ.',
resetOnlyHelper: 'ລຶບກົດປັດຈຸບັນໂດຍບໍ່ສ້າງໄຟລ໌ສຳຮອງໃໝ່. ໄຟລ໌ສຳຮອງເກົ່າຍັງຄົງຢູ່.',
backupDirectoryHelper: 'ສະແດງໄຟລ໌ສຳຮອງກົດທີ່ໃຊ້ໄດ້ໃນ {0}. ເລືອກໄຟລ໌ຫຼ້າສຸດເປັນຄ່າເລີ່ມຕົ້ນ.',
startOnly: 'ເລີ່ມຕົ້ນເທົ່ານັ້ນ',
startAndRestore: 'ເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນຈາກຂໍ້ມູນສຳຮອງ',
startWithBackupHelper: 'ເປີດໄຟວໍຕາມປົກກະຕິ ຫຼື ເລືອກຂໍ້ມູນສຳຮອງ JSON ທີ່ມີຢູ່ເພື່ອເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນກົດ.',
restoreAllRules: 'ເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນກົດ',
importRuleBackup: 'ນຳເຂົ້າຈາກຂໍ້ມູນສຳຮອງ',
initializeOnly: 'ເລີ່ມຕົ້ນເທົ່ານັ້ນ',
+14 -15
View File
@@ -1531,6 +1531,14 @@ const message = {
extraHeaders: 'Pengepala Tambahan',
},
vllmMonitor: {
concurrency: 'Permintaan serentak semasa',
sampleMoment: 'Pada masa pensampelan',
sampleWindow: 'Selang pensampelan',
requestUnit: 'permintaan',
processing: 'Sedang diproses',
queued: 'Dalam baris gilir',
requestTiming: 'Tempoh permintaan',
completedRequests: 'Permintaan selesai dalam selang masa',
tokensPerSecond: 'tokens/s',
requestsPerSecond: 'Permintaan/s',
finishReasonLabels: {
@@ -3005,20 +3013,6 @@ const message = {
remote: 'SSH jauh',
},
setting: {
panelCache: {
title: 'Tetapan cache',
tabs: 'Cache tab',
pagination: 'Cache halaman',
refresh: 'Cache sela penyegaran',
search: 'Cache input carian',
duration: 'Tempoh cache',
'24h': '24 jam',
week: 'Satu minggu',
month: 'Satu bulan',
forever: 'Kekal',
helper: 'Tempoh sah dikira dari perubahan keadaan terakhir. Selepas tamat tempoh, nilai lalai dipulihkan.',
},
panel: 'Panel',
user: 'Pengguna panel',
userChange: 'Tukar pengguna panel',
@@ -4376,10 +4370,15 @@ const message = {
familyIncomplete: 'Rantaian {0} tidak lengkap',
resetWithBackup: 'Sandarkan peraturan dan tetapkan semula',
resetOnly: 'Tetapkan semula sahaja',
resetWithBackupHelper: 'Simpan peraturan modul semasa sebagai JSON untuk dipulihkan semasa pemulaan.',
resetWithBackupHelper:
'Simpan peraturan modul semasa sebagai JSON untuk dipulihkan semasa pemulaan atau semasa tembok api dimulakan.',
resetOnlyHelper: 'Padam peraturan semasa tanpa sandaran baharu. Sandaran sedia ada dikekalkan.',
backupDirectoryHelper:
'Sandaran peraturan yang sah dalam {0} disenaraikan di bawah. Fail terkini dipilih secara lalai.',
startOnly: 'Mula sahaja',
startAndRestore: 'Mula dan pulihkan daripada sandaran',
startWithBackupHelper:
'Mulakan tembok api seperti biasa atau pilih sandaran JSON sedia ada untuk memulakan dan memulihkan peraturan.',
restoreAllRules: 'Mulakan dan pulihkan peraturan',
importRuleBackup: 'Import daripada sandaran',
initializeOnly: 'Mulakan sahaja',
+14 -15
View File
@@ -1527,6 +1527,14 @@ const message = {
extraHeaders: 'Cabeçalhos extras',
},
vllmMonitor: {
concurrency: 'Concorrência atual',
sampleMoment: 'No momento da coleta',
sampleWindow: 'Intervalo de coleta',
requestUnit: 'solicitações',
processing: 'Em processamento',
queued: 'Na fila',
requestTiming: 'Tempo das solicitações',
completedRequests: 'Solicitações concluídas no intervalo',
tokensPerSecond: 'tokens/s',
requestsPerSecond: 'Solicitações/s',
finishReasonLabels: {
@@ -3004,20 +3012,6 @@ const message = {
remote: 'SSH remoto',
},
setting: {
panelCache: {
title: 'Configurações de cache',
tabs: 'Cache de abas',
pagination: 'Cache de paginação',
refresh: 'Cache do intervalo de atualização',
search: 'Cache do campo de pesquisa',
duration: 'Duração do cache',
'24h': '24 horas',
week: 'Uma semana',
month: 'Um mês',
forever: 'Permanente',
helper: 'O prazo é contado a partir da última alteração de estado. Após a expiração, os valores padrão são restaurados.',
},
panel: 'Painel',
user: 'Usuário do painel',
userChange: 'Alterar usuário do painel',
@@ -4398,10 +4392,15 @@ const message = {
familyIncomplete: 'As cadeias de {0} estão incompletas',
resetWithBackup: 'Salvar regras e redefinir',
resetOnly: 'Apenas redefinir',
resetWithBackupHelper: 'Salve as regras do módulo atual em JSON para restaurá-las durante a inicialização.',
resetWithBackupHelper:
'Salve as regras do módulo atual em JSON para restaurá-las ao inicializar ou iniciar o firewall.',
resetOnlyHelper: 'Limpar as regras atuais sem criar um novo backup. Os backups existentes são mantidos.',
backupDirectoryHelper:
'Os backups válidos de regras em {0} são listados abaixo. O mais recente é selecionado por padrão.',
startOnly: 'Apenas iniciar',
startAndRestore: 'Iniciar e restaurar do backup',
startWithBackupHelper:
'Inicie o firewall normalmente ou escolha um backup JSON existente para iniciar e restaurar regras.',
restoreAllRules: 'Inicializar e restaurar regras',
importRuleBackup: 'Importar do backup',
initializeOnly: 'Apenas inicializar',
+14 -15
View File
@@ -1520,6 +1520,14 @@ const message = {
extraHeaders: 'Дополнительные заголовки',
},
vllmMonitor: {
concurrency: 'Текущие параллельные запросы',
sampleMoment: 'На момент замера',
sampleWindow: 'Интервал замера',
requestUnit: 'запросов',
processing: 'В обработке',
queued: 'В очереди',
requestTiming: 'Время обработки запросов',
completedRequests: 'Завершённые запросы за интервал',
tokensPerSecond: 'tokens/с',
requestsPerSecond: 'Запросов/с',
finishReasonLabels: {
@@ -2977,20 +2985,6 @@ const message = {
remote: 'Удаленный SSH',
},
setting: {
panelCache: {
title: 'Настройки кэша',
tabs: 'Кэш вкладок',
pagination: 'Кэш пагинации',
refresh: 'Кэш интервала обновления',
search: 'Кэш поискового запроса',
duration: 'Срок хранения кэша',
'24h': '24 часа',
week: 'Одна неделя',
month: 'Один месяц',
forever: 'Бессрочно',
helper: 'Срок хранения отсчитывается с последнего изменения состояния. После его истечения восстанавливаются значения по умолчанию.',
},
panel: 'Панель',
user: 'Пользователь панели',
userChange: 'Изменить пользователя панели',
@@ -4364,10 +4358,15 @@ const message = {
familyIncomplete: 'Цепочки {0} неполны',
resetWithBackup: 'Сохранить правила и сбросить',
resetOnly: 'Только сбросить',
resetWithBackupHelper: 'Сохраните правила текущего модуля в JSON для восстановления при инициализации.',
resetWithBackupHelper:
'Сохраните правила текущего модуля в JSON для восстановления при инициализации или запуске.',
resetOnlyHelper: 'Удалить текущие правила без создания новой копии. Существующие копии сохраняются.',
backupDirectoryHelper:
'Ниже перечислены действительные резервные копии правил из {0}. По умолчанию выбрана самая новая.',
startOnly: 'Только запуск',
startAndRestore: 'Запустить и восстановить из резервной копии',
startWithBackupHelper:
'Запустите брандмауэр как обычно или выберите существующую копию JSON для запуска и восстановления правил.',
restoreAllRules: 'Инициализировать и восстановить правила',
importRuleBackup: 'Импортировать из резервной копии',
initializeOnly: 'Только инициализировать',
+14 -15
View File
@@ -1527,6 +1527,14 @@ const message = {
extraHeaders: 'Ek Başlıklar',
},
vllmMonitor: {
concurrency: 'Mevcut eşzamanlı istekler',
sampleMoment: 'Örnekleme anında',
sampleWindow: 'Örnekleme aralığı',
requestUnit: 'istek',
processing: 'İşleniyor',
queued: 'Kuyrukta',
requestTiming: 'İstek süreleri',
completedRequests: 'Aralıkta tamamlanan istek sayısı',
tokensPerSecond: 'tokens/sn',
requestsPerSecond: 'İstek/sn',
finishReasonLabels: {
@@ -2995,20 +3003,6 @@ const message = {
remote: 'Uzak SSH',
},
setting: {
panelCache: {
title: 'Önbellek ayarları',
tabs: 'Sekme önbelleği',
pagination: 'Sayfalama önbelleği',
refresh: 'Yenileme aralığı önbelleği',
search: 'Arama alanı önbelleği',
duration: 'Önbellek süresi',
'24h': '24 saat',
week: 'Bir hafta',
month: 'Bir ay',
forever: 'Süresiz',
helper: 'Geçerlilik süresi son durum değişikliğinden itibaren hesaplanır. Süre dolduğunda varsayılan değerler geri yüklenir.',
},
panel: 'Panel',
user: 'Panel kullanıcısı',
userChange: 'Panel kullanıcısını değiştir',
@@ -4382,10 +4376,15 @@ const message = {
familyIncomplete: '{0} zincirleri eksik',
resetWithBackup: 'Kuralları yedekle ve sıfırla',
resetOnly: 'Yalnızca sıfırla',
resetWithBackupHelper: 'Geçerli modül kurallarını başlatma sırasında geri yüklemek için JSON olarak kaydedin.',
resetWithBackupHelper:
'Geçerli modül kurallarını ilk kurulumda veya güvenlik duvarı başlatılırken geri yüklemek için JSON olarak kaydedin.',
resetOnlyHelper: 'Yeni yedek oluşturmadan mevcut kuralları temizleyin. Önceki yedekler korunur.',
backupDirectoryHelper:
'{0} içindeki geçerli kural yedekleri listelenir. Varsayılan olarak en yeni dosya seçilir.',
startOnly: 'Yalnızca başlat',
startAndRestore: 'Başlat ve yedekten geri yükle',
startWithBackupHelper:
'Güvenlik duvarını normal başlatın veya başlatıp kuralları geri yüklemek için mevcut bir JSON yedeği seçin.',
restoreAllRules: 'Başlat ve kuralları geri yükle',
importRuleBackup: 'Yedekten içe aktar',
initializeOnly: 'Yalnızca başlat',
+12 -15
View File
@@ -1437,6 +1437,14 @@ const message = {
extraHeaders: '額外請求標頭',
},
vllmMonitor: {
concurrency: '目前並行請求',
sampleMoment: '取樣時刻',
sampleWindow: '統計區間',
requestUnit: '個請求',
processing: '處理中',
queued: '排隊中',
requestTiming: '請求耗時',
completedRequests: '區間完成請求數',
tokensPerSecond: 'tokens/秒',
requestsPerSecond: '請求/秒',
finishReasonLabels: {
@@ -2786,20 +2794,6 @@ const message = {
stopSSHWarn: '是否斷開此SSH連接',
},
setting: {
panelCache: {
title: '快取設定',
tabs: '標籤頁快取',
pagination: '分頁快取',
refresh: '重新整理頻率快取',
search: '搜尋輸入框快取',
duration: '快取時間',
'24h': '24h',
week: '一週',
month: '一個月',
forever: '永久',
helper: '有效期從狀態最後一次修改開始計算;過期後恢復預設值。',
},
panel: '面板',
user: '面板使用者',
userChange: '修改面板使用者',
@@ -4024,9 +4018,12 @@ const message = {
familyIncomplete: '{0} 鏈不完整',
resetWithBackup: '備份規則後重置',
resetOnly: '僅重置',
resetWithBackupHelper: '將目前模組的規則儲存為 JSON,之後可在初始化時選擇備份還原。',
resetWithBackupHelper: '將目前模組的規則儲存為 JSON,之後可在初始化或啟動時選擇備份還原。',
resetOnlyHelper: '不建立新備份,直接清理目前規則;保留既有歷史備份。',
backupDirectoryHelper: '自動讀取 {0} 下的有效規則備份,預設選擇最新檔案。',
startOnly: '僅啟動',
startAndRestore: '啟動並從備份還原',
startWithBackupHelper: '可正常啟動防火牆,或選擇現有的 JSON 備份啟動並還原規則。',
restoreAllRules: '初始化並還原規則',
importRuleBackup: '從備份匯入',
initializeOnly: '僅初始化',
+12 -15
View File
@@ -1450,6 +1450,14 @@ const message = {
extraHeaders: '额外请求头',
},
vllmMonitor: {
concurrency: '当前并发',
sampleMoment: '采样时刻',
sampleWindow: '统计区间',
requestUnit: '个请求',
processing: '处理中',
queued: '排队中',
requestTiming: '请求耗时',
completedRequests: '区间完成请求数',
tokensPerSecond: 'tokens/秒',
requestsPerSecond: '请求/秒',
finishReasonLabels: {
@@ -2817,20 +2825,6 @@ const message = {
stopSSHWarn: '是否断开此SSH连接',
},
setting: {
panelCache: {
title: '缓存设置',
tabs: '标签页缓存',
pagination: '翻页缓存',
refresh: '刷新频率缓存',
search: '搜索输入框缓存',
duration: '缓存时间',
'24h': '24h',
week: '一周',
month: '一月',
forever: '永久',
helper: '有效期从状态最后一次修改开始计算;过期后恢复默认值。',
},
panel: '面板',
user: '面板用户',
userChange: '修改面板用户',
@@ -4069,9 +4063,12 @@ const message = {
familyIncomplete: '{0} 链不完整',
resetWithBackup: '备份规则后重置',
resetOnly: '仅重置',
resetWithBackupHelper: '将当前模块的规则保存为 JSON,后续可在初始化时选择备份恢复。',
resetWithBackupHelper: '将当前模块的规则保存为 JSON,后续可在初始化或启动时选择备份恢复。',
resetOnlyHelper: '不生成新备份,直接清理当前规则;已有历史备份保留。',
backupDirectoryHelper: '自动读取 {0} 下的有效规则备份,默认选择最新文件。',
startOnly: '仅启动',
startAndRestore: '启动并从备份恢复',
startWithBackupHelper: '可正常启动防火墙,或选择已有的 JSON 备份启动并恢复规则。',
restoreAllRules: '初始化并恢复规则',
importRuleBackup: '从备份导入',
initializeOnly: '仅初始化',
@@ -55,7 +55,6 @@
</template>
<script setup lang="ts">
import { applyPanelCacheSettings } from '@/utils/panel-cache';
import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue';
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
import { loadingSvg } from '@/utils/svg';
@@ -142,7 +141,6 @@ const search = async () => {
settingInfo = res.data;
version.value = res.data.systemVersion;
menuAccordion.value = res.data.menuAccordion === 'Enable';
applyPanelCacheSettings(res.data.panelCache);
} catch (error) {
version.value = '';
}
-4
View File
@@ -54,7 +54,6 @@
</template>
<script setup lang="ts">
import { prunePanelCache } from '@/utils/panel-cache';
import { onMounted, computed, ref, watch, onBeforeUnmount } from 'vue';
import { Sidebar, Footer, AppMain, MobileHeader, Tabs } from './components';
import TerminalHost from '@/components/terminal/host.vue';
@@ -107,7 +106,6 @@ const loadingText = computed(() =>
);
let timer: NodeJS.Timer | null = null;
let cacheCleanupTimer: ReturnType<typeof setInterval> | undefined;
const classObj = computed(() => {
return {
@@ -188,12 +186,10 @@ const loadStatus = async () => {
}
};
onBeforeUnmount(() => {
clearInterval(cacheCleanupTimer);
clearInterval(Number(timer));
timer = null;
});
onMounted(() => {
cacheCleanupTimer = setInterval(() => prunePanelCache(), 300000);
if (openMenuTabs.value && !tabsStore.activeTabPath) {
handleMenuClick('/');
}
+10 -22
View File
@@ -1,4 +1,3 @@
import { panelCacheKey, readPanelCache, writePanelCache, removePanelCache, prunePanelCache } from '@/utils/panel-cache';
import router from '@/routers/router';
import NProgress from '@/config/nprogress';
import { useGlobalStore } from '@/composables/useGlobalStore';
@@ -103,34 +102,29 @@ router.beforeEach(async (to, from) => {
if (to.name === 'Expired') {
return true;
}
const activeMenuKey = panelCacheKey(String(to.meta.activeMenu || ''));
const activeMenuKey = 'cachedRoute' + (to.meta.activeMenu || '');
if (to.query.uncached != undefined) {
const query = { ...to.query };
delete query.uncached;
removePanelCache('tabs', activeMenuKey);
localStorage.removeItem(activeMenuKey);
return { path: to.path, query };
}
const cachedRoute = readPanelCache<string>('tabs', activeMenuKey);
const cachedRoute = localStorage.getItem(activeMenuKey);
if (
to.meta.activeMenu &&
to.meta.activeMenu != from.meta.activeMenu &&
typeof cachedRoute === 'string' &&
cachedRoute &&
cachedRoute !== to.path &&
!isRedirecting
) {
const cachedRouteInfo = router.resolve(cachedRoute);
if (
cachedRouteInfo.matched.length > 0 &&
cachedRouteInfo.meta.activeMenu === to.meta.activeMenu &&
hasRouteAccess(cachedRouteInfo)
) {
if (cachedRouteInfo.matched.length > 0 && hasRouteAccess(cachedRouteInfo)) {
isRedirecting = true;
NProgress.done();
return cachedRoute;
}
removePanelCache('tabs', activeMenuKey);
localStorage.removeItem(activeMenuKey);
}
if (!hasRouteAccess(to)) {
@@ -141,13 +135,7 @@ router.beforeEach(async (to, from) => {
return true;
});
router.afterEach((to, _from, failure) => {
prunePanelCache();
if (failure) {
isRedirecting = false;
NProgress.done();
return;
}
router.afterEach((to) => {
if (to.meta.activeMenu && !to.meta.ignoreTab && !isRedirecting) {
let notMathParam = true;
if (to.matched.some((record) => record.path.includes(':'))) {
@@ -155,13 +143,13 @@ router.afterEach((to, _from, failure) => {
}
if (notMathParam) {
if (to.meta.activeMenu === '/cronjobs' && to.path === '/cronjobs/cronjob/operate') {
writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), '/cronjobs/cronjob');
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/cronjobs/cronjob');
} else if (to.meta.activeMenu === '/containers' && to.path === '/containers/container/operate') {
writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), '/containers/container');
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/containers/container');
} else if (to.meta.activeMenu === '/toolbox' && to.path === '/toolbox/clam/setting') {
writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), '/toolbox/clam');
localStorage.setItem('cachedRoute' + to.meta.activeMenu, '/toolbox/clam');
} else {
writePanelCache('tabs', panelCacheKey(String(to.meta.activeMenu)), to.path);
localStorage.setItem('cachedRoute' + to.meta.activeMenu, to.path);
}
}
}
-3
View File
@@ -1,6 +1,5 @@
import { RouteRecordRaw } from 'vue-router';
import { DeviceType } from '@/enums/app';
import type { PanelCacheSettings } from '@/utils/panel-cache';
export interface ThemeConfigProp {
panelName: string;
primary: string;
@@ -35,8 +34,6 @@ export interface GlobalState {
isFullScreen: boolean;
openMenuTabs: boolean;
menuAccordion: boolean;
panelCache: PanelCacheSettings;
cacheUserID: string;
watermark: Watermark | null;
watermarkShow: boolean;
isLoading: boolean;
-4
View File
@@ -6,7 +6,6 @@ import { DeviceType } from '@/enums/app';
import i18n, { setActiveLocale } from '@/lang';
import { isMasterOnlyPermissionCode, setMasterOnlyPermissionCodes, toManageCode } from '@/utils/permission-codes';
import { clearPageStateCache } from '@/utils/page-state-cache';
import { defaultPanelCache } from '@/utils/panel-cache';
const CN_DOCS_URL = 'https://docs.fit2cloud.com/1panel';
const INTL_DOCS_URL = 'https://1panel.pro/docs/v2';
@@ -34,8 +33,6 @@ const GlobalStore = defineStore('GlobalState', {
isFullScreen: false,
openMenuTabs: false,
menuAccordion: false,
panelCache: { ...defaultPanelCache },
cacheUserID: '',
watermark: null,
watermarkShow: false,
isLoading: false,
@@ -126,7 +123,6 @@ const GlobalStore = defineStore('GlobalState', {
},
clearAuthInfo() {
clearPageStateCache();
this.cacheUserID = '';
this.permissions = [];
this.masterOnlyPermissions = [];
this.nodeRoles = [];
+5 -4
View File
@@ -1,5 +1,6 @@
import { bindSearchState } from '@/utils/search-reset';
import { panelCacheKey } from '@/utils/panel-cache';
import { bindSearchReset } from '@/utils/search-reset';
import router from '@/routers/router';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { defineStore } from 'pinia';
import { ref, reactive, toRef } from 'vue';
import { MsgError } from '@/utils/message';
@@ -48,7 +49,8 @@ export const ProcessStore = defineStore('ProcessStore', () => {
port: undefined,
});
const searchState = bindSearchState(() => panelCacheKey('host/process'), {
const { currentNode } = useGlobalStore();
bindSearchReset([() => router.currentRoute.value.path, currentNode], {
pid: toRef(psSearch, 'pid'),
name: toRef(psSearch, 'name'),
username: toRef(psSearch, 'username'),
@@ -319,7 +321,6 @@ export const ProcessStore = defineStore('ProcessStore', () => {
};
return {
restoreSearchState: searchState.restore,
isConnected,
isConnecting,
psData,
-130
View File
@@ -1,130 +0,0 @@
import GlobalStore from '@/store/modules/global';
export type CacheCategory = 'tabs' | 'pagination' | 'refresh' | 'search';
export interface PanelCacheSettings {
tabs: boolean;
pagination: boolean;
refresh: boolean;
search: boolean;
duration: '24h' | 'week' | 'month' | 'forever';
}
export const defaultPanelCache: PanelCacheSettings = {
tabs: true,
pagination: true,
refresh: true,
search: false,
duration: '24h',
};
const categories: CacheCategory[] = ['tabs', 'pagination', 'refresh', 'search'];
const durations = { '24h': 86400000, week: 604800000, month: 2592000000, forever: Infinity };
const prefix = '1panel:page-cache:v1:';
interface CacheEntry {
category: CacheCategory;
updatedAt: number;
value: unknown;
}
export const applyPanelCacheSettings = (raw: string) => {
let value: Partial<PanelCacheSettings> = {};
try {
value = JSON.parse(raw) || {};
} catch {
// Older servers do not return this setting yet.
}
const settings = { ...defaultPanelCache };
for (const category of categories) {
if (typeof value[category] === 'boolean') settings[category] = value[category];
}
if (Object.hasOwn(durations, value.duration || '')) settings.duration = value.duration;
const store = GlobalStore();
const changed =
settings.duration !== store.panelCache.duration ||
categories.some((category) => settings[category] !== store.panelCache[category]);
if (changed) store.panelCache = settings;
// Repeated settings loads share the normal cleanup interval.
prunePanelCache(changed);
};
export const panelCacheKey = (key: string): string => {
const { cacheUserID, currentNode } = GlobalStore();
return JSON.stringify([cacheUserID, currentNode, key]);
};
const storageKey = (category: CacheCategory, key: string) => prefix + JSON.stringify([category, key]);
const validEntry = (entry: CacheEntry, settings: PanelCacheSettings, now: number) => {
return (
entry &&
categories.includes(entry.category) &&
settings[entry.category] &&
Number.isFinite(entry.updatedAt) &&
entry.updatedAt <= now &&
now - entry.updatedAt < durations[settings.duration]
);
};
export const readPanelCache = <T>(category: CacheCategory, key: string): T | undefined => {
const store = GlobalStore();
if (!store.cacheUserID || !store.panelCache[category]) return;
try {
const target = storageKey(category, key);
const raw = localStorage.getItem(target);
if (!raw) return;
const entry: CacheEntry = JSON.parse(raw);
if (entry.category === category && validEntry(entry, store.panelCache, Date.now())) return entry.value as T;
localStorage.removeItem(target);
} catch {
// Storage can be unavailable or contain invalid data; use page defaults.
}
};
export const writePanelCache = (category: CacheCategory, key: string, value: unknown) => {
const store = GlobalStore();
if (!store.cacheUserID || !store.panelCache[category]) return;
try {
// Repeated visits and unchanged values must not extend the expiration time.
const previous = readPanelCache(category, key);
if (previous !== undefined && JSON.stringify(previous) === JSON.stringify(value)) return;
localStorage.setItem(storageKey(category, key), JSON.stringify({ category, updatedAt: Date.now(), value }));
} catch {
// A full or disabled localStorage must not block page interactions.
}
};
export const removePanelCache = (category: CacheCategory, key: string) => {
try {
localStorage.removeItem(storageKey(category, key));
} catch {
/* Storage is optional. */
}
};
let lastPruned = 0;
export const prunePanelCache = (force = false) => {
const now = Date.now();
if (!force && now - lastPruned < 300000) return;
lastPruned = now;
const settings = GlobalStore().panelCache;
try {
for (let index = localStorage.length - 1; index >= 0; index--) {
const key = localStorage.key(index);
if (!key) continue;
if (key.startsWith('1panel:search-state:v2:') || key.startsWith('cachedRoute')) {
// Legacy entries have neither ownership nor timestamps and cannot be safely restored.
localStorage.removeItem(key);
} else if (key.startsWith(prefix)) {
try {
if (validEntry(JSON.parse(localStorage.getItem(key)), settings, now)) continue;
} catch {
/* Remove malformed entries too. */
}
localStorage.removeItem(key);
}
}
} catch {
/* Storage is optional. */
}
};
export const readCachedPageSize = (key: string): number | undefined => {
const value = readPanelCache<number>('pagination', panelCacheKey(`size:${key}`));
return Number.isInteger(value) && value > 0 ? value : undefined;
};
-2
View File
@@ -22,7 +22,6 @@ export const syncAuthInfo = async (currentNode?: string) => {
const storeCurrentNode = globalStore.currentNode;
if (!globalStore.isEnterprise) {
const res = await getUserInfo();
globalStore.cacheUserID = `community:${res.data.id}`;
globalStore.setAuthInfo({
isAdmin: res.data.role === 'ADMIN',
permissions: res.data.permissions || [],
@@ -32,7 +31,6 @@ export const syncAuthInfo = async (currentNode?: string) => {
return res.data;
}
const res = await getEnterpriseUserInfo(currentNode ?? storeCurrentNode);
globalStore.cacheUserID = `enterprise:${res.data.id}`;
globalStore.setAuthInfo({
isAdmin: res.data.role === 'ADMIN',
permissions: res.data.permissions || [],
+7 -160
View File
@@ -1,165 +1,12 @@
import { storeToRefs } from 'pinia';
import { nextTick, onActivated, onDeactivated, onScopeDispose, watch, type Ref } from 'vue';
import { panelCacheKey, readPanelCache, writePanelCache } from '@/utils/panel-cache';
import GlobalStore from '@/store/modules/global';
import { watch, type Ref, type WatchSource } from 'vue';
export type SearchFields = Record<string, Ref<string | number | undefined>>;
type SearchValues = Record<string, string | number | undefined>;
// Keep categories separate so changing search text does not renew pagination's lifetime.
const searchStorage = {
save(key: string, values: SearchValues) {
for (const category of ['search', 'pagination'] as const) {
const entries = Object.entries(values).filter(
([name]) => (name === 'page' || name === 'pageSize') === (category === 'pagination'),
);
if (entries.length) {
writePanelCache(
category,
key,
Object.fromEntries(entries.map(([name, value]) => [name, value ?? null])),
);
}
}
},
read(key: string): SearchValues {
const values = {
...readPanelCache<SearchValues>('search', key),
...readPanelCache<SearchValues>('pagination', key),
};
return Object.fromEntries(
Object.entries(values)
.filter(
([, value]) =>
value === null ||
typeof value === 'string' ||
(typeof value === 'number' && Number.isFinite(value)),
)
.map(([name, value]) => [name, value === null ? undefined : value]),
);
},
};
export const bindSearchState = (key: () => string | undefined, fields: SearchFields, storage = searchStorage) => {
const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value]));
const names = Object.keys(fields).sort((left, right) => Number(left === 'page') - Number(right === 'page'));
let pending: { key: string; values: SearchValues } | undefined;
let queued = false;
const flush = () => {
if (!pending) return;
const snapshot = pending;
pending = undefined;
storage.save(snapshot.key, snapshot.values);
export const bindSearchReset = (sources: WatchSource[], fields: SearchFields) => {
const defaults = Object.values(fields).map((field) => ({ field, value: field.value }));
const reset = () => {
for (const { field, value } of defaults) field.value = value;
};
let restoring = false;
let activeKey: string | undefined;
let active = true;
let resuming = false;
let resumeVersion = 0;
const pause = () => {
flush();
active = false;
resuming = false;
resumeVersion++;
};
onScopeDispose(pause);
const restore = () => {
// Commit the previous node/resource before restoring another one.
flush();
activeKey = key();
if (!activeKey) return;
const saved = storage.read(activeKey) || defaults;
restoring = true;
try {
// Restore pagination last: search/filter watchers may reset it to page one.
for (const name of names) {
const value = Object.hasOwn(saved, name) ? saved[name] : defaults[name];
const initial = defaults[name];
const invalidPage =
(name === 'page' || name === 'pageSize') &&
(typeof value !== 'number' || !Number.isInteger(value) || value < 1);
fields[name].value =
invalidPage || (initial !== undefined && value !== undefined && typeof value !== typeof initial)
? initial
: value;
}
} finally {
restoring = false;
}
};
watch(
key,
() => {
if (active || resuming) restore();
},
{ immediate: true, flush: 'sync' },
);
watch(
() => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value])),
(values, previous) => {
if (restoring || !active || !activeKey) return;
const changed = Object.entries(values).filter(([name, value]) => value !== previous[name]);
const paginationChanged = changed.some(([name]) => name === 'page' || name === 'pageSize');
const searchChanged = changed.some(([name]) => name !== 'page' && name !== 'pageSize');
const snapshot = Object.fromEntries(
Object.entries(values).filter(([name]) =>
name === 'page' || name === 'pageSize' ? paginationChanged : searchChanged,
),
);
pending = { key: activeKey, values: { ...pending?.values, ...snapshot } };
if (queued) return;
queued = true;
queueMicrotask(() => {
queued = false;
flush();
});
},
{ flush: 'sync' },
);
return {
restore,
pause,
resume: async () => {
const version = ++resumeVersion;
resuming = true;
active = false;
restore();
const restoredKey = activeKey;
const page = fields.page?.value;
await nextTick();
if (version !== resumeVersion) return false;
// Search watchers may reset the page while restoring a changed query.
if (activeKey === restoredKey && fields.page && page !== undefined) fields.page.value = page;
active = true;
resuming = false;
return true;
},
};
};
interface SearchStateOptions {
scope?: () => string | number | undefined;
onRevisit: () => unknown;
}
/** Keep queries across page visits and reload data when a cached page becomes active. */
export const useSearchState = (page: string, fields: SearchFields, options: SearchStateOptions) => {
const { cacheUserID } = storeToRefs(GlobalStore());
const binding = bindSearchState(() => {
const scope = options.scope?.();
if (options.scope && scope === undefined) return undefined;
// Track account and node changes, including initial auth synchronisation.
if (!cacheUserID.value) return undefined;
return panelCacheKey(JSON.stringify([page, scope ?? '']));
}, fields);
let deactivated = false;
onDeactivated(() => {
deactivated = true;
binding.pause();
});
onActivated(async () => {
if (!deactivated) return;
deactivated = false;
if (await binding.resume()) return options.onRevisit();
});
watch(sources, reset, { flush: 'sync' });
return reset;
};
-2
View File
@@ -1,4 +1,3 @@
import { applyPanelCacheSettings } from '@/utils/panel-cache';
import {
getLicenseStatus,
getMasterLicenseStatus,
@@ -87,7 +86,6 @@ const loadDataFromDB = async () => {
globalStore.entrance = res.data.securityEntrance;
globalStore.openMenuTabs = res.data.menuTabs === 'Enable';
globalStore.menuAccordion = res.data.menuAccordion === 'Enable';
applyPanelCacheSettings(res.data.panelCache);
};
export async function loadProductProFromDB() {
@@ -172,7 +172,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
@@ -193,8 +193,7 @@ const searching = ref(false);
const operating = ref('');
const keyword = ref('');
const marketKeyword = ref('');
useSearchState(
'ai/agents/agent/config/tabs/plugins',
useSearchReset(
{ keyword, marketKeyword },
{
scope: () => agentId.value || undefined,
@@ -138,7 +138,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
@@ -169,8 +169,7 @@ const getDefaultMarketSource = (): HermesSkillMarketSource => {
const marketSource = ref<HermesSkillMarketSource>(getDefaultMarketSource());
const marketSearched = ref(false);
const agentId = ref(0);
useSearchState(
'ai/agents/agent/config/tabs/skills/hermes',
useSearchReset(
{ installedKeyword, marketKeyword },
{
scope: () => agentId.value || undefined,
@@ -142,7 +142,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { computed, ref, watch } from 'vue';
import { Refresh, Search } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
@@ -181,8 +181,7 @@ const getDefaultMarketSource = (): SkillMarketSource => {
const marketSource = ref<SkillMarketSource>(getDefaultMarketSource());
const marketSearched = ref(false);
const agentId = ref(0);
useSearchState(
'ai/agents/agent/config/tabs/skills/openclaw',
useSearchReset(
{ installedKeyword, marketKeyword },
{
scope: () => agentId.value || undefined,
+2 -6
View File
@@ -272,7 +272,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, defineAsyncComponent, onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { deleteAgentCheck, pageAgents, resetAgentToken, unbindAgentWebsite, updateAgentRemark } from '@/api/modules/ai';
@@ -400,11 +400,7 @@ const paginationConfig = reactive({
pageSize: 10,
total: 0,
});
useSearchState(
'ai/agents/agent/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const checkNoApp = async () => {
try {
+2 -6
View File
@@ -69,7 +69,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteAgentAccount, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
@@ -111,11 +111,7 @@ const paginationConfig = reactive({
pageSize: 10,
total: 0,
});
useSearchState(
'ai/agents/model/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async () => {
const req: AI.AgentAccountSearch = {
+2 -11
View File
@@ -98,8 +98,6 @@
</template>
<script lang="ts" setup>
import { toRef } from 'vue';
import { useSearchState } from '@/utils/search-reset';
import { AI } from '@/api/interface/ai';
import {
deleteMcpServer,
@@ -131,18 +129,11 @@ const bindDomainRef = ref();
const configRef = ref();
const items = ref<AI.McpServer[]>([]);
const paginationConfig = reactive({
cacheSizeKey: 'mcp-server-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('mcp-server-page-size')) || 20,
total: 0,
});
useSearchState(
'ai/mcp/server/index',
{
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
{ onRevisit: () => search() },
);
const getUrl = (row: AI.McpServer) => {
if (row.outputTransport == 'sse') {
+4 -7
View File
@@ -186,7 +186,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
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';
@@ -231,16 +231,13 @@ const bindDomainRef = ref();
const taskLogRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'model-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('page-size')) || 20,
total: 0,
});
const searchName = ref();
useSearchState(
'ai/model/ollama/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const appInstallID = ref(0);
const opRef = ref();
@@ -77,8 +77,6 @@
</template>
<script lang="ts" setup>
import { toRef } from 'vue';
import { useSearchState } from '@/utils/search-reset';
import OperateDialog from './operate/index.vue';
import ComposeLogs from '@/components/log/compose/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
@@ -103,14 +101,6 @@ const paginationConfig = reactive({
pageSize: 10,
total: 0,
});
useSearchState(
'ai/model/tensorrt/index',
{
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
{ onRevisit: () => search() },
);
const searchName = ref();
const opRef = ref();
const dialogRef = ref();
+6 -9
View File
@@ -70,7 +70,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { App } from '@/api/interface/app';
import { toRef, onMounted, reactive, ref } from 'vue';
import { searchApp, syncApp, syncCutomAppStore, syncLocalApp, getCurrentNodeCustomAppConfig } from '@/api/modules/app';
@@ -91,8 +91,9 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
const { isProductPro, isOffline, isMobile, isEnterprise } = useGlobalStore();
const paginationConfig = reactive({
cacheSizeKey: 'app-page-size',
currentPage: 1,
pageSize: 60,
pageSize: Number(localStorage.getItem('app-page-size')) || 60,
total: 0,
});
@@ -104,13 +105,8 @@ const req = reactive({
resource: 'all',
showCurrentArch: false,
});
useSearchState(
'app-store/apps/index',
{
search: toRef(req, 'name'),
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
useSearchReset(
{ search: toRef(req, 'name'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search(req) },
);
@@ -137,6 +133,7 @@ const search = async (req: App.AppReq) => {
loading.value = true;
req.pageSize = paginationConfig.pageSize;
req.page = paginationConfig.currentPage;
localStorage.setItem('app-page-size', req.pageSize + '');
const customReq = {
page: req.page,
@@ -139,7 +139,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import AppCard from '@/views/app-store/installed/app/card.vue';
import Backups from '@/components/backup/index.vue';
import Uploads from '@/components/upload/index.vue';
@@ -181,8 +181,9 @@ const sortContainer = ref();
let sortableInstance: Sortable | null = null;
let timer: NodeJS.Timer | null = null;
const paginationConfig = reactive({
cacheSizeKey: 'app-installed-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('app-installed-page-size')) || 20,
total: 0,
});
const open = ref(false);
@@ -210,13 +211,8 @@ const searchReq = reactive({
update: false,
sync: false,
});
useSearchState(
'app-store/installed/index',
{
search: toRef(searchReq, 'name'),
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
useSearchReset(
{ search: toRef(searchReq, 'name'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search() },
);
const router = useRouter();
@@ -242,6 +238,8 @@ const search = async () => {
searchReq.page = paginationConfig.currentPage;
searchReq.pageSize = paginationConfig.pageSize;
localStorage.setItem('app-installed-page-size', String(searchReq.pageSize));
const res = await searchAppInstalled(searchReq);
data.value = res.data.items;
paginationConfig.total = res.data.total;
@@ -522,7 +522,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { computed, h, ref } from 'vue';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import ContainerLog from '@/components/log/container/index.vue';
@@ -577,7 +577,7 @@ const dialogBackupRef = ref();
const uploadRef = ref();
const searchName = ref('');
useSearchState('container/compose/index', { search: searchName }, { onRevisit: () => search() });
useSearchReset({ search: searchName }, { onRevisit: () => search() });
const includeAppStore = ref(localStorage.getItem('includeAppStore') !== 'false');
const showType = ref('compose');
const containerStats = ref<any[]>([]);
@@ -32,11 +32,9 @@
@blur="cancelPathEditing"
@keyup.enter.prevent="submitPathInput"
/>
<el-upload ref="uploadRef" :auto-upload="false" :show-file-list="false" :on-change="onUploadChange">
<el-button class="mt-2" :loading="uploading" type="primary" plain>
{{ $t('commons.button.upload') }}
</el-button>
</el-upload>
<el-button class="mt-2" type="primary" plain @click="onUpload">
{{ $t('commons.button.upload') }}
</el-button>
<el-button class="mt-2 ml-3" plain :disabled="selectedRows.length === 0" @click="onBatchDelete">
{{ $t('commons.button.delete') }}
</el-button>
@@ -97,6 +95,7 @@
</el-form>
</template>
</DrawerPro>
<FileUpload ref="uploadRef" @close="loadContainerFiles" />
<DialogPro v-model="previewVisible" :title="previewTitle" size="w-70" :close-on-click-modal="true">
<el-alert
v-if="previewTruncated"
@@ -118,11 +117,11 @@ import {
getContainerFileContent,
getContainerFileSize,
listContainerFiles,
uploadContainerFile,
} from '@/api/modules/container';
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message';
import i18n from '@/lang';
import { ElInput, ElMessageBox, UploadFile, UploadInstance } from 'element-plus';
import { ElInput, ElMessageBox } from 'element-plus';
import FileUpload from '@/views/host/file-management/upload/index.vue';
import { Document, FolderOpened, HomeFilled } from '@element-plus/icons-vue';
import { computeSize2 } from '@/utils/size';
const visible = ref(false);
@@ -130,9 +129,8 @@ const title = ref('');
const containerID = ref('');
const filePath = ref('/');
const containerFiles = ref<any[]>([]);
const uploadRef = ref<UploadInstance>();
const uploadRef = ref<InstanceType<typeof FileUpload>>();
const pathInputRef = ref<InstanceType<typeof ElInput>>();
const uploading = ref(false);
const previewVisible = ref(false);
const previewTitle = ref('');
const previewContent = ref('');
@@ -253,27 +251,8 @@ const submitPathInput = async () => {
}
};
const onUploadChange = async (uploadFile: UploadFile) => {
if (!uploadFile.raw) {
return;
}
const formData = new FormData();
formData.append('containerID', containerID.value);
formData.append('path', filePath.value);
formData.append('file', uploadFile.raw);
uploading.value = true;
await uploadContainerFile(formData)
.then(async () => {
MsgSuccess(i18n.global.t('file.uploadSuccess'));
await loadContainerFiles();
})
.catch((err) => {
MsgError(err?.message || i18n.global.t('commons.msg.operationFailed'));
})
.finally(() => {
uploading.value = false;
uploadRef.value?.clearFiles();
});
const onUpload = () => {
uploadRef.value?.acceptParams({ path: filePath.value, containerID: containerID.value });
};
const onOpenFile = async (path: string) => {
@@ -474,7 +474,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
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';
@@ -527,14 +527,16 @@ const portFilter = ref('');
const showIPv6Ports = ref(true);
const selects = ref<any>([]);
const paginationConfig = reactive({
cacheSizeKey: 'container-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('container-page-size')) || 20,
total: 0,
state: 'all',
orderBy: 'name',
order: 'ascending',
});
const searchName = ref();
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const dialogUpgradeRef = ref();
const dialogCommitRef = ref();
const dialogPortJumpRef = ref();
@@ -550,19 +552,6 @@ const taskLogRef = ref();
const tags = ref([]);
const activeTag = ref('all');
useSearchState(
'container/container/index',
{
search: searchName,
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
state: toRef(paginationConfig, 'state'),
activeTag,
orderBy: toRef(paginationConfig, 'orderBy'),
order: toRef(paginationConfig, 'order'),
},
{ onRevisit: () => search() },
);
const activeDropdownContainerId = ref('');
const statFields = [
+5 -9
View File
@@ -157,7 +157,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, reactive, ref, computed } from 'vue';
import { dateFormat } from '@/utils/date';
import { newUUID } from '@/utils/id';
@@ -192,20 +192,16 @@ const data = ref();
const names = ref();
const repos = ref();
const paginationConfig = reactive({
cacheSizeKey: 'container-image-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('container-image-page-size')) || 20,
total: 0,
name: '',
orderBy: 'createdAt',
order: 'null',
});
useSearchState(
'container/image/index',
{
search: toRef(paginationConfig, 'name'),
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
useSearchReset(
{ search: toRef(paginationConfig, 'name'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search() },
);
const columns = ref([]);
@@ -95,7 +95,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import CreateDialog from '@/views/container/network/create/index.vue';
import DetailDrawer from '@/views/container/network/detail/index.vue';
import { toRef, reactive, ref } from 'vue';
@@ -115,16 +115,13 @@ const taskLogRef = ref();
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
cacheSizeKey: 'container-network-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('container-network-page-size')) || 20,
total: 0,
});
const searchName = ref();
useSearchState(
'container/network/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const opRef = ref();
const isActive = ref(false);
+4 -7
View File
@@ -59,7 +59,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import OperatorDialog from '@/views/container/repo/operator/index.vue';
import { toRef, reactive, ref } from 'vue';
import { dateFormat } from '@/utils/date';
@@ -72,16 +72,13 @@ const loading = ref();
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
cacheSizeKey: 'image-repo-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('image-repo-page-size')) || 20,
total: 0,
});
const searchName = ref();
useSearchState(
'container/repo/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const opRef = ref();
const confirmDialog = ref();
@@ -81,7 +81,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, reactive, ref } from 'vue';
import { dateFormat, getCurrentDateFormatted } from '@/utils/date';
import { downloadWithContent } from '@/utils/file';
@@ -105,16 +105,13 @@ const isActive = ref(false);
const isExist = ref(false);
const paginationConfig = reactive({
cacheSizeKey: 'compose-template-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('compose-template-page-size')) || 20,
total: 0,
});
const searchName = ref();
useSearchState(
'container/template/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async () => {
if (!isActive.value || !isExist.value) {
@@ -124,7 +124,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
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';
@@ -152,16 +152,13 @@ const opRef = ref();
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
cacheSizeKey: 'container-volume-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('container-volume-page-size')) || 20,
total: 0,
});
const searchName = ref();
useSearchState(
'container/volume/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const isActive = ref(false);
const isExist = ref(false);
+4 -7
View File
@@ -263,7 +263,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
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';
@@ -310,8 +310,9 @@ const groupOptions = ref();
const dialogGroupRef = ref();
const pageState = usePageState(() => ({
paginationConfig: {
cacheSizeKey: 'cronjob-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('cronjob-page-size')) || 20,
total: 0,
orderBy: 'createdAt',
order: 'null',
@@ -322,11 +323,7 @@ const pageState = usePageState(() => ({
}));
const paginationConfig = pageState.paginationConfig;
const { defaultGroupID, searchName, searchGroupID } = toRefs(pageState);
useSearchState(
'cronjob/cronjob/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
if (column) {
@@ -256,7 +256,6 @@
</template>
<script lang="ts" setup>
import { panelCacheKey, writePanelCache, readCachedPageSize } from '@/utils/panel-cache';
import { reactive, ref } from 'vue';
import { useMediaQuery } from '@vueuse/core';
import { Cronjob } from '@/api/interface/cronjob';
@@ -310,7 +309,7 @@ const formatInterval = (interval?: number) => {
};
const acceptParams = async (params: DialogProps): Promise<void> => {
let itemSize = readCachedPageSize(searchInfo.cacheSizeKey);
let itemSize = Number(localStorage.getItem(searchInfo.cacheSizeKey));
if (itemSize) {
searchInfo.pageSize = itemSize;
}
@@ -342,7 +341,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
const handleSizeChange = (val: number) => {
searchInfo.pageSize = val;
writePanelCache('pagination', panelCacheKey(`size:${searchInfo.cacheSizeKey}`), val);
localStorage.setItem(searchInfo.cacheSizeKey, val + '');
search(true);
};
const handleCurrentChange = (val: number) => {
+4 -7
View File
@@ -124,7 +124,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat, getCurrentDateFormatted } from '@/utils/date';
import { deepCopy } from '@/utils/misc';
import { newUUID } from '@/utils/id';
@@ -156,16 +156,13 @@ const scriptSync = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'script-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('script-page-size')) || 20,
total: 0,
});
const searchInfo = ref<string>('');
useSearchState(
'cronjob/library/index',
{ search: searchInfo, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchInfo, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const group = ref<string>('');
const groupOptions = ref();
@@ -323,7 +323,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, nextTick, onMounted, reactive, ref } from 'vue';
import { dateFormat } from '@/utils/date';
import { getRandomStr } from '@/utils/id';
@@ -392,17 +392,14 @@ const dbOptionsRemote = ref<Array<Database.DatabaseOption>>([]);
const tableRows = ref<Array<Database.MongodbDBInfo>>([]);
const paginationConfig = reactive({
cacheSizeKey: 'mongodb-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('mongodb-page-size')) || 20,
total: 0,
orderBy: 'createdAt',
order: 'null',
});
useSearchState(
'database/mongodb/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
@@ -75,7 +75,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat } from '@/utils/date';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
@@ -93,18 +93,15 @@ const deleteRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'mongodb-remote-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('mongodb-remote-page-size')) || 20,
total: 0,
orderBy: 'createdAt',
order: 'null',
});
const searchName = ref();
useSearchState(
'database/mongodb/remote/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
+4 -7
View File
@@ -226,7 +226,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
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';
@@ -293,18 +293,15 @@ const dialogPortJumpRef = ref();
const data = ref<Database.MysqlDBInfo[]>([]);
let grantSummaryRequestID = 0;
const paginationConfig = reactive({
cacheSizeKey: 'mysql-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('mysql-page-size')) || 20,
total: 0,
orderBy: 'createdAt',
order: 'null',
});
const searchName = ref();
useSearchState(
'database/mysql/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const mysqlContainer = ref();
const mysqlStatus = ref();
@@ -74,7 +74,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat } from '@/utils/date';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
@@ -92,18 +92,15 @@ const deleteRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'mysql-remote-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('mysql-remote-page-size')) || 20,
total: 0,
orderBy: 'createdAt',
order: 'null',
});
const searchName = ref();
useSearchState(
'database/mysql/remote/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
@@ -241,7 +241,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
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';
@@ -305,18 +305,15 @@ const dialogPortJumpRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'postgresql-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('postgresql-page-size')) || 20,
total: 0,
orderBy: 'createdAt',
order: 'null',
});
const searchName = ref();
useSearchState(
'database/postgresql/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const postgresqlContainer = ref();
const postgresqlStatus = ref();
@@ -74,7 +74,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat } from '@/utils/date';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
@@ -92,18 +92,15 @@ const deleteRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'postgresql-remote-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('postgresql-remote-page-size')) || 20,
total: 0,
orderBy: 'createdAt',
order: 'null',
});
const searchName = ref();
useSearchState(
'database/postgresql/remote/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
@@ -73,7 +73,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { dateFormat } from '@/utils/date';
import { toRef, onMounted, reactive, ref } from 'vue';
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
@@ -91,18 +91,15 @@ const deleteRef = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'redis-remote-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('redis-remote-page-size')) || 20,
total: 0,
orderBy: 'createdAt',
order: 'null',
});
const searchName = ref();
useSearchState(
'database/redis/remote/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
@@ -133,7 +133,6 @@
</template>
<script lang="ts" setup>
import { readCachedPageSize } from '@/utils/panel-cache';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { Database } from '@/api/interface/database';
import { redisPersistenceConf, updateRedisPersistenceConf } from '@/api/modules/database';
@@ -189,7 +188,7 @@ const confirmDialogRef = ref();
const paginationConfig = reactive({
cacheSizeKey: 'redis-backup-page-size',
currentPage: 1,
pageSize: readCachedPageSize('redis-backup-page-size') || 20,
pageSize: Number(localStorage.getItem('redis-backup-page-size')) || 20,
total: 0,
});
@@ -223,7 +223,6 @@
</template>
<script setup lang="ts">
import { readCachedPageSize } from '@/utils/panel-cache';
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, reactive, ref } from 'vue';
import { dateFormatSimpleWithSecond } from '@/utils/date';
import { computeSize } from '@/utils/size';
@@ -262,7 +261,7 @@ const activeCollapse = ref([]);
const paginationConfig = reactive({
cacheSizeKey: 'file-history-page-size',
currentPage: 1,
pageSize: readCachedPageSize('file-history-page-size') || 20,
pageSize: Number(localStorage.getItem('file-history-page-size')) || 20,
total: 0,
small: false,
});
@@ -132,7 +132,6 @@
</template>
<script setup lang="ts">
import { readCachedPageSize } from '@/utils/panel-cache';
import i18n from '@/lang';
import { reactive, ref } from 'vue';
import { File } from '@/api/interface/file';
@@ -227,7 +226,7 @@ const addFile = () => {
const paginationConfig = reactive({
cacheSizeKey: 'file-convert-log-page-size',
currentPage: 1,
pageSize: readCachedPageSize('file-convert-log-page-size') || 20,
pageSize: Number(localStorage.getItem('file-convert-log-page-size')) || 20,
total: 0,
});
@@ -20,7 +20,6 @@
</template>
<script setup lang="ts">
import { readCachedPageSize } from '@/utils/panel-cache';
import { searchFavorite, removeFavorite } from '@/api/modules/files';
import i18n from '@/lang';
import { reactive, ref } from 'vue';
@@ -29,7 +28,7 @@ import { File } from '@/api/interface/file';
const paginationConfig = reactive({
cacheSizeKey: 'favorite-page-size',
currentPage: 1,
pageSize: readCachedPageSize('favorite-page-size') || 20,
pageSize: Number(localStorage.getItem('favorite-page-size')) || 20,
total: 0,
});
const req = reactive({
@@ -739,7 +739,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { toRef, computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import {
addFavorite,
@@ -980,17 +980,13 @@ const getCurrentPath = () => pathRefs.value[editableTabsKey.value];
const { searchableStatus, searchablePath, setSearchableInputRef, searchableInputBlur } = useMultipleSearchable(paths);
const paginationConfig = reactive({
cacheSizeKey: 'file-page-size',
currentPage: 1,
pageSize: 100,
pageSize: Number(localStorage.getItem('file-page-size')) || 100,
total: 0,
});
useSearchState(
'host/file-management/index',
{
search: toRef(req, 'search'),
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
useSearchReset(
{ search: toRef(req, 'search'), page: toRef(paginationConfig, 'currentPage') },
{ onRevisit: () => search() },
);
@@ -82,7 +82,6 @@
</template>
<script lang="ts" setup>
import { readCachedPageSize } from '@/utils/panel-cache';
import { getRecycleStatus, clearRecycle, getRecycleList, reduceFile } from '@/api/modules/files';
import { reactive, ref } from 'vue';
import { dateFormat } from '@/utils/date';
@@ -108,7 +107,7 @@ const status = ref('Enable');
const paginationConfig = reactive({
cacheSizeKey: 'recycle-page-size',
currentPage: 1,
pageSize: readCachedPageSize('recycle-page-size') || 20,
pageSize: Number(localStorage.getItem('recycle-page-size')) || 20,
total: 0,
});
@@ -24,7 +24,6 @@
</template>
<script setup lang="ts">
import { readCachedPageSize } from '@/utils/panel-cache';
import { removeFileShare, searchFileShare } from '@/api/modules/files';
import { File } from '@/api/interface/file';
import i18n from '@/lang';
@@ -34,7 +33,7 @@ import { computed, reactive, ref } from 'vue';
const paginationConfig = reactive({
cacheSizeKey: 'share-page-size',
currentPage: 1,
pageSize: readCachedPageSize('share-page-size') || 20,
pageSize: Number(localStorage.getItem('share-page-size')) || 20,
total: 0,
});
const req = reactive({
@@ -119,8 +119,9 @@
</template>
<script setup lang="ts">
import { computed, nextTick, reactive, ref } from 'vue';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus';
import { uploadContainerFile } from '@/api/modules/container';
import { batchCheckFiles, stopChunkUpload, uploadFileData } from '@/api/modules/files';
import { CHUNK_SIZE, uploadChunkWithRetry } from '@/utils/chunk-upload';
import i18n from '@/lang';
@@ -134,6 +135,7 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
interface UploadFileProps {
path: string;
isAppendOnly?: boolean;
containerID?: string;
}
const uploadRef = ref<UploadInstance>();
@@ -154,6 +156,8 @@ const uploadOverallPercent = computed(() => {
const open = ref(false);
const isAppendOnly = ref(false);
const path = ref();
const containerID = ref('');
const targetNode = ref('');
let uploadHelper = ref('');
const dialogExistFileRef = ref();
const abortController = ref<AbortController | null>(null);
@@ -213,9 +217,6 @@ const closePage = () => {
clearFiles();
em('close', false);
};
const state = reactive({
uploadEle: null,
});
const uploaderFiles = ref<UploadFiles>([]);
const hoverIndex = ref<number | null>(null);
const tmpFiles = ref<UploadFiles>([]);
@@ -225,9 +226,11 @@ const upload = (command: string) => {
if (uploadLocked.value) {
return;
}
state.uploadEle.webkitdirectory = command == 'dir';
uploadRef.value.$el.querySelector('input').value = '';
uploadRef.value.$el.querySelector('input').click();
const input = uploadRef.value?.$el.querySelector('input[type=file]') as HTMLInputElement | null;
if (!input) return;
input.webkitdirectory = command === 'dir';
input.value = '';
input.click();
};
const removeFile = (index: number) => {
@@ -267,7 +270,7 @@ const handleDrop = async (event: DragEvent) => {
};
const convertFileToUploadFile = (file: File, path: string): UploadFile => {
const uid = Date.now();
const uid = genFileId();
const uploadRawFile: UploadRawFile = new File([file], file.name, {
type: file.type,
@@ -294,12 +297,15 @@ const traverseFileTree = async (item: any, path = '') => {
return;
}
if (item.isFile) {
if (tmpFiles.value.length > 1000) {
if (uploaderFiles.value.length + tmpFiles.value.length >= 1000) {
breakFlag.value = true;
return;
}
await new Promise<void>((resolve) => {
item.file((file: File) => {
if (uploaderFiles.value.length + tmpFiles.value.length >= 1000) {
breakFlag.value = true;
}
if (!breakFlag.value) {
tmpFiles.value.push(convertFileToUploadFile(file, path));
}
@@ -365,7 +371,14 @@ const handleSuccess: UploadProps['onSuccess'] = (res, file) => {
};
const submit = async () => {
const files = uploaderFiles.value.slice();
if (uploadLocked.value) return;
const files = containerID.value
? uploaderFiles.value.filter((file) => file.status !== 'success')
: uploaderFiles.value.slice();
if (containerID.value) {
await uploadFile(files, false);
return;
}
const fileNamesWithPath = Array.from(
new Set(files.map((file) => `${path.value}/${file.raw.webkitRelativePath || file.name}`)),
);
@@ -410,7 +423,8 @@ const uploadFile = async (files: any[], overwrite: boolean) => {
upLoading.value = true;
uploadTotalCount.value = files.length;
const controller = new AbortController();
const uploadNode = currentNode.value;
const uploadNode = containerID.value ? targetNode.value : currentNode.value;
const uploadContainerID = containerID.value;
abortController.value = controller;
let successCount = 0;
try {
@@ -426,10 +440,27 @@ const uploadFile = async (files: any[], overwrite: boolean) => {
let isSuccess = false;
try {
isSuccess =
Number(file.size) === 0
? await uploadEmptyFile(file, controller, uploadNode, overwrite)
: await uploadChunkedFile(file, controller, uploadNode, overwrite);
if (uploadContainerID) {
const data = new FormData();
data.append('containerID', uploadContainerID);
data.append('path', getUploadPath(file));
data.append('file', file.raw, getFilenameFromPath(file.raw.webkitRelativePath || file.name));
await uploadContainerFile(data, {
headers: { CurrentNode: uploadNode },
signal: controller.signal,
skipErrorMessage: true,
timeout: 0,
onUploadProgress: ({ loaded, total }) => {
uploadPercent.value = total ? Math.round((loaded / total) * 100) : 0;
},
});
isSuccess = true;
} else {
isSuccess =
Number(file.size) === 0
? await uploadEmptyFile(file, controller, uploadNode, overwrite)
: await uploadChunkedFile(file, controller, uploadNode, overwrite);
}
} catch (error) {
if (!controller.signal.aborted) {
MsgError(getErrorMessage(error));
@@ -573,6 +604,9 @@ const getFilenameFromPath = (path: string) => {
};
const acceptParams = (props: UploadFileProps) => {
if (uploadLocked.value) return;
containerID.value = props.containerID || '';
targetNode.value = currentNode.value;
path.value = props.path;
isAppendOnly.value = Boolean(props.isAppendOnly);
open.value = true;
@@ -581,22 +615,19 @@ const acceptParams = (props: UploadFileProps) => {
uploadCurrentIndex.value = 0;
uploadHelper.value = '';
canceling.value = false;
nextTick(() => {
state.uploadEle = document.querySelector('.el-upload__input');
});
};
onMounted(() => {
window.addEventListener('beforeunload', (e) => {
if (upLoading.value) {
if (upLoading.value) {
e.preventDefault();
e.returnValue = i18n.global.t('file.cancelUploadHelper');
return i18n.global.t('file.cancelUploadHelper');
}
}
});
const beforeUnload = (event: BeforeUnloadEvent) => {
if (upLoading.value) {
event.preventDefault();
event.returnValue = i18n.global.t('file.cancelUploadHelper');
}
};
onMounted(() => window.addEventListener('beforeunload', beforeUnload));
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', beforeUnload);
abortController.value?.abort();
void cleanupActiveChunkUpload();
});
defineExpose({ acceptParams, handleDrop, open });
@@ -149,3 +149,97 @@
}
}
}
.firewall-initialize-dialog.el-dialog,
.firewall-lifecycle-dialog.el-dialog {
display: flex;
flex-direction: column;
width: min(680px, calc(100vw - 32px));
max-height: calc(92dvh - 24px);
margin-top: max(24px, 8vh);
margin-bottom: 24px;
.el-dialog__body {
min-height: 0;
overflow-y: auto;
}
.firewall-rule-dialog__content {
gap: 16px;
}
.firewall-initialize-dialog__description {
margin: 0;
color: var(--el-text-color-secondary);
font-size: 13px;
line-height: 1.7;
}
.firewall-rule-dialog__docker {
padding: 14px 16px;
border-radius: 8px;
background: var(--el-fill-color-light);
.el-form-item {
margin-bottom: 0;
}
.el-form-item__label {
height: auto;
margin-bottom: 8px;
font-weight: 500;
line-height: 1.6;
}
.el-form-item__content {
display: block;
line-height: 1.6;
}
.el-radio-group {
gap: 4px 24px;
}
.el-radio {
height: auto;
min-height: 28px;
margin-right: 0;
white-space: normal;
}
}
.firewall-rule-dialog__docker-helper {
margin: 6px 0 0;
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 1.7;
}
.firewall-rule-dialog__confirmation {
padding: 14px 0 16px;
color: var(--el-text-color-regular);
text-align: left;
font-size: 13px;
line-height: 1.7;
}
.firewall-rule-dialog__files {
max-height: min(240px, 30vh);
}
.firewall-rule-dialog__modes {
align-items: stretch;
}
.firewall-initialize-dialog__backups {
display: flex;
flex-direction: column;
gap: 12px;
border-top: 1px solid var(--el-border-color-lighter);
padding-top: 16px;
}
.firewall-rule-dialog__file-name > .min-w-0 {
flex: 1;
}
}
@@ -1,12 +1,22 @@
<template>
<DialogPro
v-model="visible"
class="firewall-rule-dialog"
:title="initialize ? $t('commons.button.init') : $t('firewall.importRuleBackup')"
class="firewall-rule-dialog firewall-initialize-dialog"
:title="
starting
? $t('firewall.firewallHelper', [$t('commons.button.start')])
: initialize
? $t('commons.button.init')
: $t('firewall.importRuleBackup')
"
:show-close="!loading"
:close-on-press-escape="!loading"
>
<template #content>
<div v-loading="loading" class="firewall-rule-dialog__content">
<el-alert v-if="message" :title="message" type="warning" :closable="false" show-icon />
<slot name="before-options" />
<p v-if="message && starting" class="firewall-initialize-dialog__description">{{ message }}</p>
<el-alert v-else-if="message" :title="message" type="warning" :closable="false" show-icon />
<el-alert
v-if="backupLoadFailed"
:title="$t('firewall.ruleBackupLoadFailed')"
@@ -24,56 +34,61 @@
class="firewall-rule-dialog__modes"
:disabled="loading"
>
<el-radio value="restore" border :disabled="!loaded || backups.files.length === 0">
{{ $t('firewall.restoreAllRules') }}
<el-radio value="initialize" border>
<span class="firewall-rule-dialog__choice-title">{{ initializeLabel }}</span>
</el-radio>
<el-radio value="restore" border>
<span class="firewall-rule-dialog__choice-title">{{ restoreLabel }}</span>
</el-radio>
<el-radio value="initialize" border>{{ $t('firewall.initializeOnly') }}</el-radio>
</el-radio-group>
<div v-if="loaded" class="firewall-rule-dialog__directory">
<div v-if="loaded && mode === 'restore'" class="firewall-rule-dialog__directory">
<el-icon><FolderOpened /></el-icon>
<span>{{ $t('firewall.backupDirectoryHelper', [backups.directory]) }}</span>
</div>
<el-radio-group
v-if="mode === 'restore' && backups.files.length"
v-model="selected"
class="firewall-rule-dialog__files"
:disabled="loading"
:aria-label="$t('firewall.importRuleBackup')"
>
<el-radio v-for="file in backups.files" :key="file.name" :value="file.name" border>
<span class="firewall-rule-dialog__file-name">
<el-icon><Document /></el-icon>
<span class="min-w-0">{{ file.name }}</span>
<el-button
v-permission
class="shrink-0"
type="primary"
link
size="small"
:disabled="loading"
:aria-label="`${$t('firewall.downloadRuleBackup')}: ${file.name}`"
@click.stop.prevent="
downloadFile(`${backups.directory}/${file.name}`, globalStore.currentNode)
"
>
{{ $t('commons.button.download') }}
</el-button>
</span>
<span class="firewall-rule-dialog__file-meta">
<el-tag size="small" type="info" effect="plain">{{ file.provider }}</el-tag>
<span>{{ $t('firewall.backupRuleCount') }}: {{ file.ruleCount }}</span>
<span>{{ new Date(file.modifiedAt).toLocaleString() }}</span>
</span>
</el-radio>
</el-radio-group>
<el-empty
v-else-if="loaded && !backups.files.length"
:description="$t('firewall.noRuleBackup')"
:image-size="64"
/>
<div v-if="mode === 'restore'" class="firewall-initialize-dialog__backups">
<el-radio-group
v-if="backups.files.length"
v-model="selected"
class="firewall-rule-dialog__files"
:disabled="loading"
:aria-label="$t('firewall.importRuleBackup')"
>
<el-radio v-for="file in backups.files" :key="file.name" :value="file.name" border>
<span class="firewall-rule-dialog__file-name">
<el-icon><Document /></el-icon>
<span class="min-w-0">{{ file.name }}</span>
<el-button
v-permission
class="shrink-0"
type="primary"
link
size="small"
:disabled="loading"
:aria-label="`${$t('firewall.downloadRuleBackup')}: ${file.name}`"
@click.stop.prevent="
downloadFile(`${backups.directory}/${file.name}`, globalStore.currentNode)
"
>
{{ $t('commons.button.download') }}
</el-button>
</span>
<span class="firewall-rule-dialog__file-meta">
<el-tag size="small" type="info" effect="plain">{{ file.provider }}</el-tag>
<span>{{ $t('firewall.backupRuleCount') }}: {{ file.ruleCount }}</span>
<span>{{ new Date(file.modifiedAt).toLocaleString() }}</span>
</span>
</el-radio>
</el-radio-group>
<el-empty
v-else-if="!initialize && loaded && !backups.files.length"
:description="$t('firewall.noRuleBackup')"
:image-size="64"
/>
</div>
</div>
</template>
<template #footer>
<slot name="confirmation" />
<el-button :disabled="loading" @click="visible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button
type="primary"
@@ -81,13 +96,7 @@
:disabled="loading || (mode === 'restore' && (!loaded || !selected))"
@click="submit"
>
{{
initialize
? mode === 'restore'
? $t('firewall.restoreAllRules')
: $t('firewall.initializeOnly')
: $t('commons.button.import')
}}
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</DialogPro>
@@ -95,7 +104,8 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import i18n from '@/lang';
import { Document, FolderOpened } from '@element-plus/icons-vue';
import DialogPro from '@/components/dialog-pro/index.vue';
import { Firewall } from '@/api/interface/firewall';
@@ -110,13 +120,24 @@ import { GlobalStore } from '@/store';
import { downloadFile } from '@/utils/file';
const globalStore = GlobalStore();
const emit = defineEmits<{ (event: 'initialize'): void; (event: 'complete'): void }>();
const emit = defineEmits<{
(event: 'initialize'): void;
(event: 'restore', backupFile: string): void;
(event: 'complete'): void;
}>();
const visible = ref(false);
const loading = ref(false);
const loaded = ref(false);
const backupLoadFailed = ref(false);
const message = ref('');
const initialize = ref(true);
const starting = ref(false);
const initializeLabel = computed(() =>
i18n.global.t(starting.value ? 'firewall.startOnly' : 'firewall.initializeOnly'),
);
const restoreLabel = computed(() =>
i18n.global.t(starting.value ? 'firewall.startAndRestore' : 'firewall.restoreAllRules'),
);
const subsystem = ref<Firewall.BackendSubsystem>('system');
const mode = ref<'restore' | 'initialize'>('initialize');
const selected = ref('');
@@ -127,8 +148,10 @@ const acceptParams = async (
warning: string,
withInitialization = true,
target: Firewall.BackendSubsystem = 'system',
start = false,
) => {
subsystem.value = target;
starting.value = start;
initialize.value = withInitialization;
message.value = warning;
visible.value = true;
@@ -146,7 +169,7 @@ const loadBackups = async () => {
try {
backups.value = (await listFirewallRuleBackups(subsystem.value)).data;
selected.value = backups.value.files[0]?.name || '';
mode.value = selected.value || !initialize.value ? 'restore' : 'initialize';
mode.value = !starting.value && (selected.value || !initialize.value) ? 'restore' : 'initialize';
loaded.value = true;
} catch {
backupLoadFailed.value = true;
@@ -163,6 +186,11 @@ const submit = async () => {
return;
}
if (!loaded.value || !selected.value) return;
if (starting.value) {
visible.value = false;
emit('restore', selected.value);
return;
}
loading.value = true;
try {
const response =
@@ -219,7 +219,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
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';
@@ -263,8 +263,7 @@ const openRuleTask = (taskID: string) => taskLogRef.value?.openWithTaskID(taskID
const orphanDrawerVisible = ref(false);
const orphanSearchName = ref('');
const searchName = ref('');
useSearchState(
'host/firewall/docker/index',
useSearchReset(
{ search: searchName },
{
onRevisit: () => {
@@ -117,7 +117,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import OperateDialog from './operate/index.vue';
import ImportDialog from './import/index.vue';
import FireRouter from '@/views/host/firewall/index.vue';
@@ -183,15 +183,12 @@ const operateRules = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'firewall-forward-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('firewall-forward-page-size')) || 20,
total: 0,
});
useSearchState(
'host/firewall/forward/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const search = async () => {
if (!isInit.value || !isBind.value || fireName.value === '-') {
@@ -302,7 +302,7 @@
</template>
<script lang="ts" setup>
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { Firewall } from '@/api/interface/firewall';
import type { FuTableOperationButton } from '@/components/table/shared';
import { buildHostRuleExport } from './transfer';
@@ -427,15 +427,12 @@ const supportsFirewalldPriority = computed(() => {
return major > 0 || minor >= 7;
});
const paginationConfig = reactive({
cacheSizeKey: 'firewall-rule-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('firewall-rule-page-size')) || 20,
total: 0,
});
useSearchState(
'host/firewall/rule/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const providerScopes = (): Firewall.Scope[] => {
if (provider.value === 'iptables' || provider.value === 'nftables') {
@@ -57,7 +57,7 @@
v-node-admin
type="primary"
:disabled="lifecycleBusy"
@click="onOperate('start')"
@click="onInit"
link
>
{{ $t('commons.button.start') }}
@@ -145,18 +145,58 @@
:title="$t('firewall.directBackendConflictWarning', [backendName, baseInfo.conflictBackend])"
/>
</div>
<DockerRestart
ref="dockerRef"
v-model:withDockerRestart="withDockerRestart"
@submit="onSubmit"
<DialogPro
v-model="operationVisible"
class="firewall-rule-dialog firewall-lifecycle-dialog"
:title="$t('firewall.firewallHelper', [$t('commons.button.' + operation)])"
>
<template #helper>
<span>{{ $t('firewall.' + operation + 'FirewallHelper') }}</span>
<template #content>
<el-form class="firewall-rule-dialog__docker" label-position="top" @submit.prevent>
<el-form-item :label="$t('firewall.dockerRestart')">
<el-radio-group v-model="withDockerRestart">
<el-radio :value="true">{{ $t('setting.restartNow') }}</el-radio>
<el-radio :value="false">{{ $t('setting.restartLater') }}</el-radio>
</el-radio-group>
<p class="firewall-rule-dialog__docker-helper">
{{ $t(withDockerRestart ? 'xpack.node.syncProxyHelper1' : 'xpack.node.syncProxyHelper2') }}
</p>
</el-form-item>
</el-form>
</template>
</DockerRestart>
<template #footer>
<div class="firewall-rule-dialog__confirmation">
<span>{{ $t('firewall.' + operation + 'FirewallHelper') }}</span>
</div>
<el-button @click="operationVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit">{{ $t('commons.button.confirm') }}</el-button>
</template>
</DialogPro>
<TaskLog ref="taskLogRef" @close="handleInitializationTaskClose" />
<RuleInitialize ref="initializeRef" @initialize="initializeOnly" @complete="loadBaseInfo" />
<RuleInitialize
ref="initializeRef"
@initialize="initializeOnly"
@restore="onOperate('start', $event)"
@complete="loadBaseInfo"
>
<template v-if="isServiceBackend && dockerStatus" #before-options>
<el-form class="firewall-rule-dialog__docker" label-position="top" @submit.prevent>
<el-form-item :label="$t('firewall.dockerRestart')">
<el-radio-group v-model="withDockerRestart">
<el-radio :value="true">{{ $t('setting.restartNow') }}</el-radio>
<el-radio :value="false">{{ $t('setting.restartLater') }}</el-radio>
</el-radio-group>
<p class="firewall-rule-dialog__docker-helper">
{{ $t(withDockerRestart ? 'xpack.node.syncProxyHelper1' : 'xpack.node.syncProxyHelper2') }}
</p>
</el-form-item>
</el-form>
</template>
<template v-if="isServiceBackend" #confirmation>
<div class="firewall-rule-dialog__confirmation">
<span>{{ $t('firewall.startFirewallHelper') }}</span>
</div>
</template>
</RuleInitialize>
</div>
</template>
@@ -171,7 +211,7 @@ import {
} from '@/api/modules/firewall';
import i18n from '@/lang';
import NoSuchService from '@/components/layout-content/no-such-service.vue';
import DockerRestart from '@/components/docker-proxy/docker-restart.vue';
import DialogPro from '@/components/dialog-pro/index.vue';
import { MsgSuccess } from '@/utils/message';
import { ElMessageBox } from 'element-plus';
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
@@ -187,7 +227,7 @@ import { useGlobalStore } from '@/composables/useGlobalStore';
const props = defineProps({
currentTab: String,
});
const { docsUrl } = useGlobalStore();
const { docsUrl, isIntl } = useGlobalStore();
const goToFirewallSetting = () => routerToName('FirewallSetting');
const goToScriptLibrary = () => routerToNameWithQuery('Library', { uncached: 'true' });
@@ -206,8 +246,9 @@ const baseInfo = ref<Firewall.FirewallBase>({
ipv4: { available: false, initialized: false, bound: false },
ipv6: { available: false, initialized: false, bound: false },
});
const dockerRef = ref();
const operationVisible = ref(false);
const operation = ref('restart');
const restoreBackupFile = ref('');
const dockerStatus = ref();
const withDockerRestart = ref(false);
const taskLogRef = ref();
@@ -276,7 +317,7 @@ const familyActions = computed(() =>
message: familyIssueText(issue),
helpUrl:
issue.available && issue.reason === 'ipv6_ra_required'
? `${docsUrl.value}/user_manual/hosts/firewall`
? `${docsUrl.value}/user_manual/${isIntl.value ? 'system' : 'hosts'}/firewall`
: undefined,
action,
};
@@ -365,12 +406,20 @@ const loadDocker = async () => {
const initializeRef = ref<InstanceType<typeof RuleInitialize>>();
const onInit = async () => {
if (lifecycleBusy.value) return;
if (isServiceBackend.value) withDockerRestart.value = !!dockerStatus.value;
const name = baseInfo.value.name || backendName.value;
const message =
props.currentTab === 'base' && baseInfo.value.conflictBackend
? i18n.global.t('firewall.initDirectBackendConflictMsg', [name, baseInfo.value.conflictBackend])
: i18n.global.t('firewall.initMsg', [name]);
await initializeRef.value?.acceptParams(message, true, props.currentTab === 'forward' ? 'forwarding' : 'system');
const message = isServiceBackend.value
? i18n.global.t('firewall.startWithBackupHelper')
: props.currentTab === 'base' && baseInfo.value.conflictBackend
? i18n.global.t('firewall.initDirectBackendConflictMsg', [name, baseInfo.value.conflictBackend])
: i18n.global.t('firewall.initMsg', [name]);
await initializeRef.value?.acceptParams(
message,
true,
props.currentTab === 'forward' ? 'forwarding' : 'system',
isServiceBackend.value,
);
};
const initializeOnly = async () => {
@@ -422,17 +471,22 @@ const onUnBind = async () => {
await loadBaseInfo();
};
const onOperate = async (op: string) => {
const onOperate = async (op: string, backupFile = '') => {
if (lifecycleBusy.value) return;
operation.value = op;
if (backendName.value === 'iptables' || backendName.value === 'nftables' || !dockerStatus.value) {
restoreBackupFile.value = backupFile;
if (op === 'start') {
await submitLifecycleOperation(!!dockerStatus.value && withDockerRestart.value);
} else if (backendName.value === 'iptables' || backendName.value === 'nftables' || !dockerStatus.value) {
await submitLifecycleOperation(false);
} else {
dockerRef.value.acceptParams({ title: i18n.global.t('firewall.dockerRestart') });
withDockerRestart.value = true;
operationVisible.value = true;
}
};
const onSubmit = async () => {
operationVisible.value = false;
await submitLifecycleOperation(withDockerRestart.value);
};
@@ -447,7 +501,7 @@ const submitLifecycleOperation = async (restartDocker: boolean) => {
lifecycleSubmitting.value = true;
emit('update:loading', true);
try {
const result = (await operateFire(operation.value, restartDocker)).data;
const result = (await operateFire(operation.value, restartDocker, restoreBackupFile.value || undefined)).data;
if (disposed) return;
if (result?.queued && result.taskID) {
baseInfo.value.lifecycleTaskID = result.taskID;
@@ -86,8 +86,6 @@ const statusOptions = [
const { currentNode } = useGlobalStore();
const isCompactTable = useMediaQuery('(max-width: 1024px)');
const processStore = ProcessStore();
processStore.restoreSearchState();
onActivated(() => processStore.restoreSearchState());
const quickSearchName = (name: string) => {
processStore.netSearch.processID = undefined;
@@ -83,8 +83,6 @@ import { useMediaQuery } from '@vueuse/core';
const { currentNode } = useGlobalStore();
const isCompactTable = useMediaQuery('(max-width: 1024px)');
const processStore = ProcessStore();
processStore.restoreSearchState();
onActivated(() => processStore.restoreSearchState());
const permissionDirective = resolveDirective('permission');
const statusOptions = computed(() => [
+4 -7
View File
@@ -93,7 +93,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { dateFormatServerTimezone } from '@/utils/date';
import { downloadFile } from '@/utils/file';
@@ -108,8 +108,9 @@ const loading = ref();
const data = ref();
const confirmDialogRef = ref();
const paginationConfig = reactive({
cacheSizeKey: 'ssh-log-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('ssh-log-page-size')) || 20,
total: 0,
});
@@ -119,11 +120,7 @@ const exportConfig = reactive({
status: 'All',
});
const searchInfo = ref();
useSearchState(
'host/ssh/log/log',
{ search: searchInfo, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchInfo, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const searchStatus = ref('All');
const search = async () => {
@@ -24,7 +24,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import FireRouter from '@/views/host/ssh/index.vue';
import { toRef, ref, onMounted, onUnmounted, reactive } from 'vue';
import i18n from '@/lang';
@@ -38,7 +38,7 @@ const sshSearch = reactive({
type: 'ssh',
loginUser: '',
});
useSearchState('host/ssh/session/index', { search: toRef(sshSearch, 'loginUser') }, { onRevisit: () => search() });
useSearchReset({ search: toRef(sshSearch, 'loginUser') }, { onRevisit: () => search() });
const buttons = [
{
@@ -93,7 +93,6 @@
</div>
</template>
<script lang="ts" setup>
import { readCachedPageSize } from '@/utils/panel-cache';
import { Host } from '@/api/interface/host';
import { deleteCert, searchCert, syncCert } from '@/api/modules/host';
import i18n from '@/lang';
@@ -110,7 +109,7 @@ const selects = ref<any>([]);
const paginationConfig = reactive({
cacheSizeKey: 'login-log-page-size',
currentPage: 1,
pageSize: readCachedPageSize('login-log-page-size') || 20,
pageSize: Number(localStorage.getItem('login-log-page-size')) || 20,
total: 0,
small: true,
});
+4 -5
View File
@@ -101,8 +101,7 @@
</template>
<script setup lang="ts">
import { panelCacheKey, writePanelCache, readCachedPageSize } from '@/utils/panel-cache';
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { Log } from '@/api/interface/log';
import { getSystemLogStatus, listRunningServices, readSystemLogs } from '@/api/modules/log';
@@ -115,7 +114,7 @@ const logs = ref<Log.SystemLogItem[]>([]);
const loading = ref(false);
const watching = ref(false);
const keyword = ref('');
useSearchState('log/host-system/index', { keyword }, { onRevisit: () => scheduleFilterSearch() });
useSearchReset({ keyword }, { onRevisit: () => scheduleFilterSearch() });
const priority = ref('');
const service = ref('');
const services = ref<string[]>([]);
@@ -195,7 +194,7 @@ const loadPreviousPage = () => {
};
const changePageSize = () => {
writePanelCache('pagination', panelCacheKey(`size:${paginationConfig.cacheSizeKey}`), paginationConfig.pageSize);
localStorage.setItem(paginationConfig.cacheSizeKey, String(paginationConfig.pageSize));
resetAndLoadLogs();
};
@@ -264,7 +263,7 @@ const priorityType = (value: string) => {
};
onMounted(async () => {
const cachedPageSize = readCachedPageSize(paginationConfig.cacheSizeKey);
const cachedPageSize = Number(localStorage.getItem(paginationConfig.cacheSizeKey));
if (pageSizes.includes(cachedPageSize)) {
paginationConfig.pageSize = cachedPageSize;
}
+4 -7
View File
@@ -45,7 +45,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import LogRouter from '@/views/log/router/index.vue';
import { dateFormat } from '@/utils/date';
@@ -61,16 +61,13 @@ const loading = ref();
const data = ref();
const confirmDialogRef = ref();
const paginationConfig = reactive({
cacheSizeKey: 'login-log-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('login-log-page-size')) || 20,
total: 0,
});
const searchInfo = ref<string>('');
useSearchState(
'log/login/index',
{ search: searchInfo, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchInfo, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const searchStatus = ref<string>('');
const search = async () => {
+4 -7
View File
@@ -139,7 +139,7 @@
</template>
<script setup lang="ts">
import { useSearchState } from '@/utils/search-reset';
import { useSearchReset } from '@/composables/useSearchReset';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import LogRouter from '@/views/log/router/index.vue';
import { dateFormat } from '@/utils/date';
@@ -154,16 +154,13 @@ const loading = ref();
const data = ref();
const confirmDialogRef = ref();
const paginationConfig = reactive({
cacheSizeKey: 'operation-log-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('operation-log-page-size')) || 20,
total: 0,
});
const searchName = ref<string>('');
useSearchState(
'log/operation/index',
{ search: searchName, page: toRef(paginationConfig, 'currentPage'), pageSize: toRef(paginationConfig, 'pageSize') },
{ onRevisit: () => search() },
);
useSearchReset({ search: searchName, page: toRef(paginationConfig, 'currentPage') }, { onRevisit: () => search() });
const searchGroup = ref<string>('');
const searchStatus = ref<string>('');
const searchNode = ref<string>('');
+2 -11
View File
@@ -49,8 +49,6 @@
</template>
<script setup lang="ts">
import { toRef } from 'vue';
import { useSearchState } from '@/utils/search-reset';
import LogRouter from '@/views/log/router/index.vue';
import { dateFormat } from '@/utils/date';
import { searchTasks } from '@/api/modules/log';
@@ -63,18 +61,11 @@ import { translateTaskText } from '@/utils/task';
const loading = ref();
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'task-page-size',
currentPage: 1,
pageSize: 20,
pageSize: Number(localStorage.getItem('task-page-size')) || 20,
total: 0,
});
useSearchState(
'log/task/index',
{
page: toRef(paginationConfig, 'currentPage'),
pageSize: toRef(paginationConfig, 'pageSize'),
},
{ onRevisit: () => search() },
);
const taskLogRef = ref();
const req = reactive({
type: '',
@@ -357,7 +357,6 @@
</template>
<script setup lang="ts">
import { applyPanelCacheSettings } from '@/utils/panel-cache';
import { ref, reactive, onMounted, onBeforeUnmount, computed, nextTick } from 'vue';
import type { ElForm } from 'element-plus';
import {
@@ -927,7 +926,6 @@ const getSetting = async () => {
i18n.warnHtmlMessage = false;
openMenuTabs.value = res.data.menuTabs === 'Enable';
menuAccordion.value = res.data.menuAccordion === 'Enable';
applyPanelCacheSettings(res.data.panelCache);
themeConfig.value = { ...themeConfig.value, theme: res.data.theme, panelName: res.data.panelName };
if (res.data.passkeySetting && !isIntl.value && !isFxplay.value) {

Some files were not shown because too many files have changed in this diff Show More