Compare commits

...
Author SHA1 Message Date
ssongliu c695a2a408 fix(firewall): prepare access whitelist before startup (#14114) 2026-10-16 17:27:48 +08:00
ssongliu 690cb48280 feat(vllm): refine monitor snapshots and historical request counts (#14109) 2026-10-16 13:36:29 +08:00
ssongliu f226fed8cd feat(firewall): support restoring rule backups on startup (#14108) 2026-10-16 10:50:07 +08:00
Yuki Guo 110c292856 chore: chore home default css (#14101) 2026-10-15 15:45:25 +08:00
Kadxyandssongliu 8e865c2c46 fix: stop translating the built-in Default group name (#14100)
* fix: stop translating the built-in Default group name

* fix: resolve default cronjob group by flag

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-10-15 15:42:18 +08:00
Kadxy 17b8f472e7 fix(cronjob): show Yes/No text instead of check icon in script library interactive column (#14099) 2026-10-15 15:25:03 +08:00
Yuki Guo d9dc5d7aa9 fix: alert config log text (#14086)
* fix: alert config log text

* fix: alert config log text
2026-10-15 09:33:23 +08:00
53 changed files with 802 additions and 440 deletions
+8 -5
View File
@@ -293,7 +293,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/update [post]
// @x-panel-log {"bodyKeys":["id","displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][displayName]","formatEN":"update alert config [id][displayName]"}
// @x-panel-log {"bodyKeys":["displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [displayName]","formatEN":"update alert config [displayName]"}
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
var req dto.AlertConfigUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -321,13 +321,15 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/status [post]
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [name][status]","formatEN":"update alert config status [name][status]"}
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
var req dto.AlertConfigStatusUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
name, err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c))
helper.SetOperationLogValues(c, map[string]string{"name": name})
if err != nil {
helper.InternalServer(c, err)
return
}
@@ -353,13 +355,14 @@ func loadAuditUser(c *gin.Context) string {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [id]","formatEN":"delete alert config [id]"}
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [name]","formatEN":"delete alert config [name]"}
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
var req dto.DeleteRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := alertService.DeleteAlertConfig(req.ID)
name, err := alertService.DeleteAlertConfig(req.ID)
helper.SetOperationLogValues(c, map[string]string{"name": name})
if err != nil {
helper.InternalServer(c, err)
return
+21
View File
@@ -3,6 +3,7 @@ package helper
import (
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
@@ -184,3 +185,23 @@ func GetParamInt32(paramName string, c *gin.Context) (int32, error) {
intNum, _ := strconv.Atoi(idParam)
return int32(intNum), nil
}
// SetOperationLogValues supplies server-resolved values to Core's operation log.
// Call before writing the response. Only pass public audit fields, never credentials.
func SetOperationLogValues(c *gin.Context, values map[string]string) {
const header = "X-Op-Resolved"
resolved := make(map[string]string)
if data, err := base64.RawURLEncoding.DecodeString(c.Writer.Header().Get(header)); err == nil {
_ = json.Unmarshal(data, &resolved)
}
if resolved == nil {
resolved = make(map[string]string)
}
for key, value := range values {
resolved[key] = value
}
data, err := json.Marshal(resolved)
if err == nil {
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
}
}
+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
+26 -14
View File
@@ -74,8 +74,8 @@ type IAlertService interface {
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
DeleteAlertConfig(id uint) error
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error)
DeleteAlertConfig(id uint) (string, error)
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
TestCustomAlertConfig(req dto.AlertConfigTest, lang string) (dto.AlertConfigTestResult, error)
}
@@ -623,25 +623,26 @@ func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operato
})
}
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) (string, error) {
if err := validateAlertConfigStatus(req.Status); err != nil {
return err
return "", err
}
config, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return err
return "", err
}
name := alertConfigLogName(config.Config, config.Title)
if req.Status == constant.AlertEnable {
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
return err
return name, err
}
if config.Type == constant.Custom {
if _, err := alertwebhook.Resolve(config); err != nil {
return err
return name, err
}
}
}
return alertRepo.UpdateAlertConfig(map[string]interface{}{
return name, alertRepo.UpdateAlertConfig(map[string]interface{}{
"status": req.Status,
"update_user": operator,
}, repo.WithByID(req.ID))
@@ -813,19 +814,20 @@ func alertConfigSMSPhone(configData string) string {
return strings.TrimSpace(cfg.Phone)
}
func (a AlertService) DeleteAlertConfig(id uint) error {
_, err := alertRepo.GetConfigById(id)
func (a AlertService) DeleteAlertConfig(id uint) (string, error) {
config, err := alertRepo.GetConfigById(id)
if err != nil {
return err
return "", err
}
name := alertConfigLogName(config.Config, config.Title)
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
if err != nil {
return err
return name, err
}
if len(usedAlerts) > 0 {
return buserr.New("ErrAlertConfigInUse")
return name, buserr.New("ErrAlertConfigInUse")
}
return alertRepo.DeleteAlertConfig(repo.WithByID(id))
return name, alertRepo.DeleteAlertConfig(repo.WithByID(id))
}
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
@@ -1043,3 +1045,13 @@ func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method s
_, err = a.validateAlertMethodReferences(method)
return err
}
func alertConfigLogName(config, title string) string {
var value struct {
DisplayName string `json:"displayName"`
}
if json.Unmarshal([]byte(config), &value) == nil && strings.TrimSpace(value.DisplayName) != "" {
return strings.TrimSpace(value.DisplayName)
}
return title
}
+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)
}
+10 -14
View File
@@ -108,33 +108,29 @@
"formatEN": "create alert [title]"
},
"/alert/config/del": {
"bodyKeys": [
"id"
],
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除告警配置 [id]",
"formatEN": "delete alert config [id]"
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"beforeFunctions": []
},
"/alert/config/status": {
"bodyKeys": [
"id",
"status"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置状态 [id][status]",
"formatEN": "update alert config status [id][status]"
"formatZH": "更新告警配置状态 [name][status]",
"formatEN": "update alert config status [name][status]",
"beforeFunctions": []
},
"/alert/config/update": {
"bodyKeys": [
"id",
"displayName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置 [id][displayName]",
"formatEN": "update alert config [id][displayName]"
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"beforeFunctions": []
},
"/alert/del": {
"bodyKeys": [
+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{})
},
}
+9 -12
View File
@@ -3906,13 +3906,11 @@ const docTemplate = `{
"Alert"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"id"
],
"formatEN": "delete alert config [id]",
"formatZH": "删除告警配置 [id]",
"paramKeys": []
"bodyKeys": [],
"paramKeys": [],
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"BeforeFunctions": []
}
}
},
@@ -4041,14 +4039,13 @@ const docTemplate = `{
"Alert"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"id",
"displayName"
],
"formatEN": "update alert config [id][displayName]",
"formatZH": "更新告警配置 [id][displayName]",
"paramKeys": []
"paramKeys": [],
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"BeforeFunctions": []
}
}
},
+9 -12
View File
@@ -3902,13 +3902,11 @@
"Alert"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"id"
],
"formatEN": "delete alert config [id]",
"formatZH": "删除告警配置 [id]",
"paramKeys": []
"bodyKeys": [],
"paramKeys": [],
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"BeforeFunctions": []
}
}
},
@@ -4037,14 +4035,13 @@
"Alert"
],
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"id",
"displayName"
],
"formatEN": "update alert config [id][displayName]",
"formatZH": "更新告警配置 [id][displayName]",
"paramKeys": []
"paramKeys": [],
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"BeforeFunctions": []
}
}
},
+10 -14
View File
@@ -108,33 +108,29 @@
"formatEN": "create alert [title]"
},
"/alert/config/del": {
"bodyKeys": [
"id"
],
"bodyKeys": [],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "删除告警配置 [id]",
"formatEN": "delete alert config [id]"
"formatZH": "删除告警配置 [name]",
"formatEN": "delete alert config [name]",
"beforeFunctions": []
},
"/alert/config/status": {
"bodyKeys": [
"id",
"status"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置状态 [id][status]",
"formatEN": "update alert config status [id][status]"
"formatZH": "更新告警配置状态 [name][status]",
"formatEN": "update alert config status [name][status]",
"beforeFunctions": []
},
"/alert/config/update": {
"bodyKeys": [
"id",
"displayName"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "更新告警配置 [id][displayName]",
"formatEN": "update alert config [id][displayName]"
"formatZH": "更新告警配置 [displayName]",
"formatEN": "update alert config [displayName]",
"beforeFunctions": []
},
"/alert/del": {
"bodyKeys": [
+2 -4
View File
@@ -133,11 +133,9 @@ func OperationLog() gin.HandlerFunc {
record.AuthMethod = "session"
}
// Handlers may supply audit values even without database resolution metadata.
mergeResolvedData(writer.resolvedHeader, formatMap)
if len(operationDic.BeforeFunctions) != 0 {
if needAgentResolve {
mergeResolvedData(writer.resolvedHeader, formatMap)
}
if allowCoreFallback && !hasAllResolvedData(formatMap, operationDic.BeforeFunctions) {
dbItem, err := newDB(record.Path)
if err == nil {
+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,
);
@@ -8,10 +8,7 @@
<el-table-column :label="$t('commons.table.name')" prop="name">
<template #default="{ row }">
<div v-if="!row.edit">
<span v-if="row.name === 'Default'">
{{ $t('commons.table.default') }}
</span>
<span v-if="row.name !== 'Default'">{{ row.name }}</span>
<span>{{ row.name }}</span>
<el-tag v-if="row.isDefault" type="success" class="ml-2" size="small">
({{ $t('commons.table.default') }})
</el-tag>
+1 -4
View File
@@ -8,10 +8,7 @@
<el-table-column :label="$t('commons.table.name')" prop="name">
<template #default="{ row }">
<div v-if="!row.edit">
<span v-if="row.name === 'Default'">
{{ $t('commons.table.default') }}
</span>
<span v-if="row.name !== 'Default'">{{ row.name }}</span>
<span>{{ row.name }}</span>
<el-tag v-if="row.isDefault" type="success" class="ml-2" size="small">
({{ $t('commons.table.default') }})
</el-tag>
@@ -154,7 +154,7 @@ const connectionTree = computed<ConnectionTreeItem[]>(() => {
groups.push(
...hostTree.value.map((group): ConnectionTreeItem => ({
id: `host-group-${group.id}`,
label: group.label === 'Default' ? i18n.global.t('commons.table.default') : group.label,
label: group.label,
kind: 'group',
children: (group.children || []).map((host) => ({
id: `host-${host.id}`,
@@ -38,14 +38,7 @@
</el-form-item>
<el-form-item v-if="!form.isLocal" :label="$t('commons.table.group')" prop="groupID">
<el-select filterable v-model="form.groupID" clearable style="width: 100%">
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item v-if="!form.isLocal" :label="$t('commons.table.title')" prop="name">
@@ -4,11 +4,8 @@
<script lang="ts" setup>
import { onMounted, nextTick, watch, onBeforeUnmount, ref } from 'vue';
import echarts from '@/utils/echarts';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { themeConfig } = useGlobalStore();
const isDarkTheme = ref(false);
const PieChartRef = ref<HTMLElement>();
let mediaQuery: MediaQueryList;
let themeObserver: MutationObserver | undefined;
let resizeObserver: ResizeObserver | undefined;
let resizeFrame: number | undefined;
@@ -47,16 +44,17 @@ function resizeChart() {
echarts.getInstanceByDom(PieChartRef.value)?.resize();
}
function getThemeColors() {
// Match the applied page theme, including while settings are still loading.
const isDarkTheme = document.documentElement.classList.contains('dark');
const style = getComputedStyle(document.documentElement);
return {
primaryLight2: getComputedStyle(document.documentElement)
.getPropertyValue('--panel-color-primary-light-3')
.trim(),
primaryLight1: getComputedStyle(document.documentElement).getPropertyValue('--panel-color-primary').trim(),
pieBgColor: isDarkTheme.value ? '#434552' : '#ffffff',
textColor: isDarkTheme.value ? '#ffffff' : '#0f0f0f',
subtextColor: isDarkTheme.value ? '#BBBFC4' : '#646A73',
shadowColor: isDarkTheme.value ? '#16191D' : 'rgba(0, 94, 235, 0.1)',
backgroundStyleColor: isDarkTheme.value ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 94, 235, 0.05)',
primaryLight2: style.getPropertyValue('--panel-color-primary-light-3').trim() || '#4c8ef1',
primaryLight1: style.getPropertyValue('--panel-color-primary').trim() || '#005eeb',
pieBgColor: isDarkTheme ? '#434552' : '#ffffff',
textColor: isDarkTheme ? '#ffffff' : '#0f0f0f',
subtextColor: isDarkTheme ? '#BBBFC4' : '#646A73',
shadowColor: isDarkTheme ? '#16191D' : 'rgba(0, 94, 235, 0.1)',
backgroundStyleColor: isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 94, 235, 0.05)',
};
}
@@ -64,11 +62,6 @@ function initChart() {
if (!PieChartRef.value) {
return;
}
if (themeConfig.value.theme === 'auto') {
isDarkTheme.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
} else {
isDarkTheme.value = themeConfig.value.theme === 'dark';
}
let myChart = echarts?.getInstanceByDom(PieChartRef.value);
if (myChart === null || myChart === undefined) {
myChart = echarts.init(PieChartRef.value);
@@ -194,8 +187,11 @@ function handleThemeChange() {
onMounted(() => {
nextTick(() => {
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', handleThemeChange);
themeObserver = new MutationObserver(handleThemeChange);
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'style'],
});
initChart();
window.addEventListener('resize', changeChartSize);
if (PieChartRef.value) {
@@ -214,7 +210,7 @@ onBeforeUnmount(() => {
echarts.getInstanceByDom(PieChartRef.value)?.dispose();
}
window.removeEventListener('resize', changeChartSize);
mediaQuery?.removeEventListener('change', handleThemeChange);
themeObserver?.disconnect();
resizeObserver?.disconnect();
});
</script>
+14 -1
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: {
@@ -4299,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 -1
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: {
@@ -4347,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 -1
View File
@@ -1502,6 +1502,14 @@ const message = {
extraHeaders: 'هدرهای اضافی',
},
vllmMonitor: {
concurrency: 'درخواست‌های هم‌زمان فعلی',
sampleMoment: 'در لحظه نمونه‌برداری',
sampleWindow: 'بازه نمونه‌برداری',
requestUnit: 'درخواست',
processing: 'در حال پردازش',
queued: 'در صف',
requestTiming: 'زمان پردازش درخواست',
completedRequests: 'تعداد درخواست‌های تکمیل‌شده در بازه',
tokensPerSecond: 'tokens/ثانیه',
requestsPerSecond: 'درخواست/ثانیه',
finishReasonLabels: {
@@ -4259,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 -1
View File
@@ -1505,6 +1505,14 @@ const message = {
extraHeaders: '追加ヘッダー',
},
vllmMonitor: {
concurrency: '現在の同時リクエスト',
sampleMoment: 'サンプリング時点',
sampleWindow: '集計区間',
requestUnit: '件',
processing: '処理中',
queued: '待機中',
requestTiming: 'リクエスト処理時間',
completedRequests: '区間内の完了リクエスト数',
tokensPerSecond: 'tokens/秒',
requestsPerSecond: 'リクエスト/秒',
finishReasonLabels: {
@@ -4282,9 +4290,12 @@ const message = {
familyIncomplete: '{0} のチェーンが不完全です',
resetWithBackup: 'ルールをバックアップしてリセット',
resetOnly: 'リセットのみ',
resetWithBackupHelper: '現在のモジュールのルールを JSON に保存し、初期化時に復元できます。',
resetWithBackupHelper: '現在のモジュールのルールを JSON に保存し、初期化時または起動時に復元できます。',
resetOnlyHelper: '新しいバックアップを作成せずに現在のルールを消去します。既存のバックアップは保持されます。',
backupDirectoryHelper: '{0} 内の有効なルールバックアップを表示します。既定では最新のファイルが選択されます。',
startOnly: '起動のみ',
startAndRestore: '起動してバックアップから復元',
startWithBackupHelper: '通常起動するか、既存の JSON バックアップを選んで起動とルールの復元を行います。',
restoreAllRules: '初期化してルールを復元',
importRuleBackup: 'バックアップからインポート',
initializeOnly: '初期化のみ',
+12 -1
View File
@@ -1490,6 +1490,14 @@ const message = {
extraHeaders: '추가 헤더',
},
vllmMonitor: {
concurrency: '현재 동시 요청',
sampleMoment: '샘플링 시점',
sampleWindow: '집계 구간',
requestUnit: '개 요청',
processing: '처리 중',
queued: '대기 중',
requestTiming: '요청 소요 시간',
completedRequests: '구간 내 완료 요청 수',
tokensPerSecond: 'tokens/초',
requestsPerSecond: '요청/초',
finishReasonLabels: {
@@ -4204,9 +4212,12 @@ const message = {
familyIncomplete: '{0} 체인이 불완전합니다',
resetWithBackup: '규칙 백업 후 초기화',
resetOnly: '초기화만 수행',
resetWithBackupHelper: '현재 모듈의 규칙을 JSON으로 저장하여 초기화 시 복원할 수 있습니다.',
resetWithBackupHelper: '현재 모듈의 규칙을 JSON으로 저장하여 초기화 또는 시작 시 복원할 수 있습니다.',
resetOnlyHelper: '새 백업 없이 현재 규칙을 삭제합니다. 기존 백업은 유지됩니다.',
backupDirectoryHelper: '{0}의 유효한 규칙 백업을 표시합니다. 기본적으로 최신 파일이 선택됩니다.',
startOnly: '시작만',
startAndRestore: '시작 및 백업에서 복원',
startWithBackupHelper: '방화벽을 일반적으로 시작하거나 기존 JSON 백업을 선택하여 시작하고 규칙을 복원합니다.',
restoreAllRules: '초기화 및 규칙 복원',
importRuleBackup: '백업에서 가져오기',
initializeOnly: '초기화만 수행',
+12 -1
View File
@@ -1484,6 +1484,14 @@ const message = {
extraHeaders: 'Headers ເພີ່ມເຕີມ',
},
vllmMonitor: {
concurrency: 'ຄຳຮ້ອງຂໍພ້ອມກັນໃນປັດຈຸບັນ',
sampleMoment: 'ໃນເວລາເກັບຕົວຢ່າງ',
sampleWindow: 'ຊ່ວງເວລາເກັບຕົວຢ່າງ',
requestUnit: 'ຄຳຮ້ອງຂໍ',
processing: 'ກຳລັງປະມວນຜົນ',
queued: 'ກຳລັງລໍຖ້າ',
requestTiming: 'ເວລາປະມວນຜົນຄຳຮ້ອງຂໍ',
completedRequests: 'ຈຳນວນຄຳຮ້ອງຂໍທີ່ສຳເລັດໃນຊ່ວງເວລາ',
tokensPerSecond: 'tokens/ວິນາທີ',
requestsPerSecond: 'ຄຳຮ້ອງຂໍ/ວິນາທີ',
finishReasonLabels: {
@@ -4173,9 +4181,12 @@ const message = {
familyIncomplete: 'ຊຸດກົດ {0} ບໍ່ຄົບຖ້ວນ',
resetWithBackup: 'ສຳຮອງກົດແລ້ວຣີເຊັດ',
resetOnly: 'ຣີເຊັດເທົ່ານັ້ນ',
resetWithBackupHelper: 'ບັນທຶກກົດຂອງໂມດູນປັດຈຸບັນເປັນ JSON ເພື່ອກູ້ຄືນເມື່ອເລີ່ມຕົ້ນ.',
resetWithBackupHelper: 'ບັນທຶກກົດຂອງໂມດູນປັດຈຸບັນເປັນ JSON ເພື່ອກູ້ຄືນເມື່ອເລີ່ມຕົ້ນ ຫຼື ເປີດໃຊ້ໄຟວໍ.',
resetOnlyHelper: 'ລຶບກົດປັດຈຸບັນໂດຍບໍ່ສ້າງໄຟລ໌ສຳຮອງໃໝ່. ໄຟລ໌ສຳຮອງເກົ່າຍັງຄົງຢູ່.',
backupDirectoryHelper: 'ສະແດງໄຟລ໌ສຳຮອງກົດທີ່ໃຊ້ໄດ້ໃນ {0}. ເລືອກໄຟລ໌ຫຼ້າສຸດເປັນຄ່າເລີ່ມຕົ້ນ.',
startOnly: 'ເລີ່ມຕົ້ນເທົ່ານັ້ນ',
startAndRestore: 'ເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນຈາກຂໍ້ມູນສຳຮອງ',
startWithBackupHelper: 'ເປີດໄຟວໍຕາມປົກກະຕິ ຫຼື ເລືອກຂໍ້ມູນສຳຮອງ JSON ທີ່ມີຢູ່ເພື່ອເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນກົດ.',
restoreAllRules: 'ເລີ່ມຕົ້ນ ແລະ ກູ້ຄືນກົດ',
importRuleBackup: 'ນຳເຂົ້າຈາກຂໍ້ມູນສຳຮອງ',
initializeOnly: 'ເລີ່ມຕົ້ນເທົ່ານັ້ນ',
+14 -1
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: {
@@ -4362,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 -1
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: {
@@ -4384,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 -1
View File
@@ -1520,6 +1520,14 @@ const message = {
extraHeaders: 'Дополнительные заголовки',
},
vllmMonitor: {
concurrency: 'Текущие параллельные запросы',
sampleMoment: 'На момент замера',
sampleWindow: 'Интервал замера',
requestUnit: 'запросов',
processing: 'В обработке',
queued: 'В очереди',
requestTiming: 'Время обработки запросов',
completedRequests: 'Завершённые запросы за интервал',
tokensPerSecond: 'tokens/с',
requestsPerSecond: 'Запросов/с',
finishReasonLabels: {
@@ -4350,10 +4358,15 @@ const message = {
familyIncomplete: 'Цепочки {0} неполны',
resetWithBackup: 'Сохранить правила и сбросить',
resetOnly: 'Только сбросить',
resetWithBackupHelper: 'Сохраните правила текущего модуля в JSON для восстановления при инициализации.',
resetWithBackupHelper:
'Сохраните правила текущего модуля в JSON для восстановления при инициализации или запуске.',
resetOnlyHelper: 'Удалить текущие правила без создания новой копии. Существующие копии сохраняются.',
backupDirectoryHelper:
'Ниже перечислены действительные резервные копии правил из {0}. По умолчанию выбрана самая новая.',
startOnly: 'Только запуск',
startAndRestore: 'Запустить и восстановить из резервной копии',
startWithBackupHelper:
'Запустите брандмауэр как обычно или выберите существующую копию JSON для запуска и восстановления правил.',
restoreAllRules: 'Инициализировать и восстановить правила',
importRuleBackup: 'Импортировать из резервной копии',
initializeOnly: 'Только инициализировать',
+14 -1
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: {
@@ -4368,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 -1
View File
@@ -1437,6 +1437,14 @@ const message = {
extraHeaders: '額外請求標頭',
},
vllmMonitor: {
concurrency: '目前並行請求',
sampleMoment: '取樣時刻',
sampleWindow: '統計區間',
requestUnit: '個請求',
processing: '處理中',
queued: '排隊中',
requestTiming: '請求耗時',
completedRequests: '區間完成請求數',
tokensPerSecond: 'tokens/秒',
requestsPerSecond: '請求/秒',
finishReasonLabels: {
@@ -4010,9 +4018,12 @@ const message = {
familyIncomplete: '{0} 鏈不完整',
resetWithBackup: '備份規則後重置',
resetOnly: '僅重置',
resetWithBackupHelper: '將目前模組的規則儲存為 JSON,之後可在初始化時選擇備份還原。',
resetWithBackupHelper: '將目前模組的規則儲存為 JSON,之後可在初始化或啟動時選擇備份還原。',
resetOnlyHelper: '不建立新備份,直接清理目前規則;保留既有歷史備份。',
backupDirectoryHelper: '自動讀取 {0} 下的有效規則備份,預設選擇最新檔案。',
startOnly: '僅啟動',
startAndRestore: '啟動並從備份還原',
startWithBackupHelper: '可正常啟動防火牆,或選擇現有的 JSON 備份啟動並還原規則。',
restoreAllRules: '初始化並還原規則',
importRuleBackup: '從備份匯入',
initializeOnly: '僅初始化',
+12 -1
View File
@@ -1450,6 +1450,14 @@ const message = {
extraHeaders: '额外请求头',
},
vllmMonitor: {
concurrency: '当前并发',
sampleMoment: '采样时刻',
sampleWindow: '统计区间',
requestUnit: '个请求',
processing: '处理中',
queued: '排队中',
requestTiming: '请求耗时',
completedRequests: '区间完成请求数',
tokensPerSecond: 'tokens/秒',
requestsPerSecond: '请求/秒',
finishReasonLabels: {
@@ -4055,9 +4063,12 @@ const message = {
familyIncomplete: '{0} 链不完整',
resetWithBackup: '备份规则后重置',
resetOnly: '仅重置',
resetWithBackupHelper: '将当前模块的规则保存为 JSON,后续可在初始化时选择备份恢复。',
resetWithBackupHelper: '将当前模块的规则保存为 JSON,后续可在初始化或启动时选择备份恢复。',
resetOnlyHelper: '不生成新备份,直接清理当前规则;已有历史备份保留。',
backupDirectoryHelper: '自动读取 {0} 下的有效规则备份,默认选择最新文件。',
startOnly: '仅启动',
startAndRestore: '启动并从备份恢复',
startWithBackupHelper: '可正常启动防火墙,或选择已有的 JSON 备份启动并恢复规则。',
restoreAllRules: '初始化并恢复规则',
importRuleBackup: '从备份导入',
initializeOnly: '仅初始化',
@@ -163,9 +163,6 @@ const displayNodeName = (item: NodeItem) => {
};
const displayGroupName = (item: NodeItem) => {
if (item.groupBelong === 'Default') {
return i18n.global.t('commons.table.default');
}
return item.group || item.groupBelong || i18n.global.t('commons.table.default');
};
+9 -25
View File
@@ -45,14 +45,7 @@
<TableViewSwitch v-model="viewMode" storage-key="cronjob" />
<el-select v-model="searchGroupID" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('commons.table.group') }}</template>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="searchName" />
<TableRefresh @search="search()" />
@@ -104,16 +97,14 @@
@change="updateGroup(row)"
>
<template #read>
{{ row.groupBelong === 'Default' ? $t('commons.table.default') : row.groupBelong }}
{{ row.groupBelong }}
</template>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option
v-for="item in groupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</fu-select-rw-switch>
</template>
</el-table-column>
@@ -474,17 +465,10 @@ const loadGroups = async () => {
searchGroupID.value = undefined;
paginationConfig.currentPage = 1;
}
for (const group of groupOptions.value) {
if (group.name === 'Default') {
defaultGroupID.value = group.id;
break;
}
}
defaultGroupID.value = groupOptions.value.find((group) => group.isDefault)?.id;
for (const item of data.value) {
if (item.groupID === 0) {
item.groupBelong = 'Default';
item.groupID = defaultGroupID.value;
continue;
}
let hasGroup = false;
for (const group of groupOptions.value) {
@@ -80,14 +80,12 @@
<LayoutCol>
<el-form-item :label="$t('commons.table.group')" prop="groupID">
<el-select filterable v-model="form.groupID" clearable>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option
v-for="item in groupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</LayoutCol>
+10 -18
View File
@@ -45,14 +45,7 @@
<template #rightToolBar>
<el-select v-model="group" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('commons.table.group') }}</template>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="searchInfo" />
<TableRefresh @search="search()" />
@@ -69,7 +62,12 @@
:heightDiff="300"
>
<el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.name')" show-overflow-tooltip prop="name" min-width="60">
<el-table-column
:label="$t('commons.table.name')"
show-overflow-tooltip
prop="name"
min-width="140"
>
<template #default="{ row }">
<el-text type="primary" class="cursor-pointer" @click="showScript(row)">
{{ row.name }}
@@ -78,21 +76,15 @@
</el-table-column>
<el-table-column :label="$t('cronjob.library.isInteractive')" prop="isInteractive" min-width="60">
<template #default="{ row }">
<div class="-mb-1">
<el-icon v-if="row.isInteractive"><Check /></el-icon>
<el-icon v-else><Minus /></el-icon>
</div>
{{ $t(row.isInteractive ? 'commons.true' : 'commons.false') }}
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.group')" min-width="120" prop="group">
<el-table-column :label="$t('commons.table.group')" min-width="100" prop="group">
<template #default="{ row }">
<el-button class="mr-3" size="small" v-if="row.isSystem">{{ $t('menu.system') }}</el-button>
<span v-if="row.groupBelong">
<el-button size="small" v-for="(item, index) in row.groupBelong" :key="index">
<span v-if="item === 'Default'">
{{ $t('commons.table.default') }}
</span>
<span v-else>{{ item }}</span>
{{ item }}
</el-button>
</span>
</template>
@@ -21,14 +21,7 @@
</el-form-item>
<el-form-item :label="$t('commons.table.group')" prop="groupList">
<el-select filterable v-model="dialogData.rowData!.groupList" multiple>
<div v-for="item in groupOptions" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<span class="input-help">{{ $t('cronjob.library.groupHelper') }}</span>
</el-form-item>
@@ -33,7 +33,6 @@
import { ref, nextTick } from 'vue';
import Terminal from '@/components/terminal/index.vue';
import { getCommandTree } from '@/api/modules/command';
import i18n from '@/lang';
const terminalVisible = ref(false);
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
@@ -64,11 +63,6 @@ const initTerm = async (cwd: string) => {
const loadCommandTree = async () => {
const res = await getCommandTree('command');
commandTree.value = res.data || [];
for (const item of commandTree.value) {
if (item.label === 'Default') {
item.label = i18n.global.t('commons.table.default');
}
}
};
function quickInput(val: Array<string>) {
@@ -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 =
@@ -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;
@@ -27,10 +27,7 @@
@change="changeGroup"
class="p-w-200 ml-2 mt-2"
>
<div v-for="item in groupList" :key="item.id">
<el-option v-if="item.name === 'Default'" :label="$t('commons.table.default')" :value="item.id" />
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<el-card class="mt-2 w-full" v-loading="loading">
@@ -54,12 +51,7 @@
min-width="80"
prop="groupBelong"
fix
>
<template #default="{ row }">
<span v-if="row.groupBelong === 'Default'">{{ $t('commons.table.default') }}</span>
<span v-else>{{ row.groupBelong }}</span>
</template>
</el-table-column>
/>
<el-table-column
:label="$t('terminal.command')"
:min-width="120"
+8 -17
View File
@@ -27,14 +27,7 @@
<template #rightToolBar>
<el-select v-model="group" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('commons.table.group') }}</template>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="info" />
<TableRefresh @search="search()" />
@@ -74,16 +67,14 @@
<template #default="{ row }">
<fu-select-rw-switch v-model="row.groupID" @change="updateGroup(row)">
<template #read>
{{ row.groupBelong === 'Default' ? $t('commons.table.default') : row.groupBelong }}
{{ row.groupBelong }}
</template>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option
v-for="item in groupList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</fu-select-rw-switch>
</template>
</el-table-column>
@@ -22,14 +22,7 @@
</el-form-item>
<el-form-item :label="$t('commons.table.group')" prop="groupID">
<el-select filterable v-model="dialogData.rowData!.groupID" clearable style="width: 100%">
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item v-if="!batchMode" :label="$t('terminal.command')" prop="command">
+8 -17
View File
@@ -16,14 +16,7 @@
<el-select v-model="group" @change="search()" clearable class="p-w-200">
<template #prefix>{{ $t('commons.table.group') }}</template>
<el-option :label="$t('commons.table.all')" value=""></el-option>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="info" />
<TableRefresh @search="search()" />
@@ -48,16 +41,14 @@
<template #default="{ row }">
<fu-select-rw-switch v-model="row.groupID" @change="updateGroup(row)">
<template #read>
{{ row.groupBelong === 'Default' ? $t('commons.table.default') : row.groupBelong }}
{{ row.groupBelong }}
</template>
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option
v-for="item in groupList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</fu-select-rw-switch>
</template>
</el-table-column>
@@ -50,14 +50,7 @@
</el-form-item>
<el-form-item :label="$t('commons.table.group')" prop="groupID">
<el-select filterable v-model="dialogData.rowData!.groupID" clearable style="width: 100%">
<div v-for="item in groupList" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="$t('commons.table.title')" prop="name">
@@ -268,11 +268,6 @@ const handleTabsRemove = async (targetName: string, action: 'remove' | 'add') =>
const loadCommandTree = async () => {
const res = await getCommandTree('command');
commandTree.value = res.data || [];
for (const item of commandTree.value) {
if (item.label === 'Default') {
item.label = i18n.global.t('commons.table.default');
}
}
};
const sendToTerminals = (command: string, all: boolean) => {
@@ -1,12 +1,7 @@
<template>
<el-form-item :label="$t('commons.table.group')" :prop="prop">
<el-select v-model="selectedValue">
<el-option
v-for="(group, index) in groups"
:key="index"
:label="group.name === 'Default' ? $t('commons.table.default') : group.name"
:value="group.id"
/>
<el-option v-for="(group, index) in groups" :key="index" :label="group.name" :value="group.id" />
</el-select>
</el-form-item>
</template>
+1 -8
View File
@@ -45,14 +45,7 @@
<el-select v-model="req.websiteGroupId" @change="search()" class="p-w-200" :disabled="disabledConfig">
<template #prefix>{{ $t('commons.table.group') }}</template>
<el-option :label="$t('commons.table.all')" :value="0"></el-option>
<div v-for="item in groups" :key="item.id">
<el-option
v-if="item.name === 'Default'"
:label="$t('commons.table.default')"
:value="item.id"
/>
<el-option v-else :label="item.name" :value="item.id" />
</div>
<el-option v-for="item in groups" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
<TableSearch @search="search()" v-model:searchName="req.name" :disabled="disabledConfig" />
<TableRefresh @search="search()" :disabled="disabledConfig" />