mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-19 06:31:54 +00:00
reduce duplication
This commit is contained in:
@@ -1,16 +1,24 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/config"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/balance"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/erasure_coding"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks/vacuum"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
@@ -161,19 +169,10 @@ func (h *MaintenanceHandlers) ShowTaskConfig(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTaskConfig updates configuration for a specific task type
|
||||
// UpdateTaskConfig updates task configuration from form
|
||||
func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) {
|
||||
taskTypeName := c.Param("taskType")
|
||||
|
||||
// Get the task type
|
||||
taskType := maintenance.GetMaintenanceTaskType(taskTypeName)
|
||||
if taskType == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Task type not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Try to get templ UI provider first - temporarily disabled
|
||||
// templUIProvider := getTemplUIProvider(taskType)
|
||||
taskType := types.TaskType(taskTypeName)
|
||||
|
||||
// Parse form data
|
||||
err := c.Request.ParseForm()
|
||||
@@ -182,31 +181,51 @@ func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert form data to map
|
||||
formData := make(map[string][]string)
|
||||
for key, values := range c.Request.PostForm {
|
||||
formData[key] = values
|
||||
// Get the task configuration schema
|
||||
schema := tasks.GetTaskConfigSchema(taskTypeName)
|
||||
if schema == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Schema not found for task type: " + taskTypeName})
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new config instance based on task type and apply schema defaults
|
||||
var config interface{}
|
||||
switch taskType {
|
||||
case types.TaskTypeVacuum:
|
||||
config = &vacuum.VacuumConfig{}
|
||||
case types.TaskTypeBalance:
|
||||
config = &balance.BalanceConfig{}
|
||||
case types.TaskTypeErasureCoding:
|
||||
config = &erasure_coding.ErasureCodingConfig{}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported task type: " + taskTypeName})
|
||||
return
|
||||
}
|
||||
|
||||
// Temporarily disabled templ UI provider
|
||||
// if templUIProvider != nil {
|
||||
// // Use the new templ-based UI provider
|
||||
// config, err = templUIProvider.ParseConfigForm(formData)
|
||||
// if err != nil {
|
||||
// c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()})
|
||||
// return
|
||||
// }
|
||||
// // Apply configuration using templ provider
|
||||
// err = templUIProvider.ApplyConfig(config)
|
||||
// if err != nil {
|
||||
// c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()})
|
||||
// return
|
||||
// }
|
||||
// } else {
|
||||
// Fallback to old UI provider for tasks that haven't been migrated yet
|
||||
// Fallback to old UI provider for tasks that haven't been migrated yet
|
||||
// Apply schema defaults first
|
||||
if err := schema.ApplyDefaults(config); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply defaults: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse form data using schema-based approach
|
||||
err = h.parseTaskConfigFromForm(c.Request.PostForm, schema, config)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the configuration
|
||||
if validationErrors := schema.ValidateConfig(config); len(validationErrors) > 0 {
|
||||
errorMessages := make([]string, len(validationErrors))
|
||||
for i, err := range validationErrors {
|
||||
errorMessages[i] = err.Error()
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Configuration validation failed", "details": errorMessages})
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration using UIProvider
|
||||
uiRegistry := tasks.GetGlobalUIRegistry()
|
||||
typesRegistry := tasks.GetGlobalTypesRegistry()
|
||||
|
||||
@@ -223,25 +242,123 @@ func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse configuration from form using old provider
|
||||
config, err = provider.ParseConfigForm(formData)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse configuration: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration using old provider
|
||||
// Apply configuration using provider
|
||||
err = provider.ApplyConfig(config)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()})
|
||||
return
|
||||
}
|
||||
// } // End of disabled templ UI provider else block
|
||||
|
||||
// Redirect back to task configuration page
|
||||
c.Redirect(http.StatusSeeOther, "/maintenance/config/"+taskTypeName)
|
||||
}
|
||||
|
||||
// parseTaskConfigFromForm parses form data using schema definitions
|
||||
func (h *MaintenanceHandlers) parseTaskConfigFromForm(formData map[string][]string, schema *tasks.TaskConfigSchema, config interface{}) error {
|
||||
configValue := reflect.ValueOf(config)
|
||||
if configValue.Kind() == reflect.Ptr {
|
||||
configValue = configValue.Elem()
|
||||
}
|
||||
|
||||
if configValue.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("config must be a struct or pointer to struct")
|
||||
}
|
||||
|
||||
configType := configValue.Type()
|
||||
|
||||
for i := 0; i < configValue.NumField(); i++ {
|
||||
field := configValue.Field(i)
|
||||
fieldType := configType.Field(i)
|
||||
|
||||
// Get JSON tag name
|
||||
jsonTag := fieldType.Tag.Get("json")
|
||||
if jsonTag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove options like ",omitempty"
|
||||
if commaIdx := strings.Index(jsonTag, ","); commaIdx > 0 {
|
||||
jsonTag = jsonTag[:commaIdx]
|
||||
}
|
||||
|
||||
// Find corresponding schema field
|
||||
schemaField := schema.GetFieldByName(jsonTag)
|
||||
if schemaField == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse value based on field type
|
||||
if err := h.parseFieldFromForm(formData, schemaField, field); err != nil {
|
||||
return fmt.Errorf("error parsing field %s: %w", schemaField.DisplayName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseFieldFromForm parses a single field value from form data
|
||||
func (h *MaintenanceHandlers) parseFieldFromForm(formData map[string][]string, schemaField *config.Field, fieldValue reflect.Value) error {
|
||||
if !fieldValue.CanSet() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch schemaField.Type {
|
||||
case config.FieldTypeBool:
|
||||
// Checkbox fields - present means true, absent means false
|
||||
_, exists := formData[schemaField.JSONName]
|
||||
fieldValue.SetBool(exists)
|
||||
|
||||
case config.FieldTypeInt:
|
||||
if values, ok := formData[schemaField.JSONName]; ok && len(values) > 0 {
|
||||
if intVal, err := strconv.Atoi(values[0]); err != nil {
|
||||
return fmt.Errorf("invalid integer value: %s", values[0])
|
||||
} else {
|
||||
fieldValue.SetInt(int64(intVal))
|
||||
}
|
||||
}
|
||||
|
||||
case config.FieldTypeFloat:
|
||||
if values, ok := formData[schemaField.JSONName]; ok && len(values) > 0 {
|
||||
if floatVal, err := strconv.ParseFloat(values[0], 64); err != nil {
|
||||
return fmt.Errorf("invalid float value: %s", values[0])
|
||||
} else {
|
||||
fieldValue.SetFloat(floatVal)
|
||||
}
|
||||
}
|
||||
|
||||
case config.FieldTypeString:
|
||||
if values, ok := formData[schemaField.JSONName]; ok && len(values) > 0 {
|
||||
fieldValue.SetString(values[0])
|
||||
}
|
||||
|
||||
case config.FieldTypeInterval:
|
||||
// Parse interval fields with value + unit
|
||||
valueKey := schemaField.JSONName + "_value"
|
||||
unitKey := schemaField.JSONName + "_unit"
|
||||
|
||||
if valueStrs, ok := formData[valueKey]; ok && len(valueStrs) > 0 {
|
||||
value, err := strconv.Atoi(valueStrs[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid interval value: %s", valueStrs[0])
|
||||
}
|
||||
|
||||
unit := "minutes" // default
|
||||
if unitStrs, ok := formData[unitKey]; ok && len(unitStrs) > 0 {
|
||||
unit = unitStrs[0]
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
seconds := config.IntervalValueUnitToSeconds(value, unit)
|
||||
fieldValue.SetInt(int64(seconds))
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported field type: %s", schemaField.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateMaintenanceConfig updates maintenance configuration from form
|
||||
func (h *MaintenanceHandlers) UpdateMaintenanceConfig(c *gin.Context) {
|
||||
var config maintenance.MaintenanceConfig
|
||||
|
||||
+52
-279
@@ -1,12 +1,10 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
@@ -44,285 +42,24 @@ func (ui *UIProvider) GetIcon() string {
|
||||
return "fas fa-balance-scale text-secondary"
|
||||
}
|
||||
|
||||
// BalanceConfig represents the balance configuration
|
||||
// BalanceConfig represents the balance configuration matching the schema
|
||||
type BalanceConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ImbalanceThreshold float64 `json:"imbalance_threshold"`
|
||||
ScanIntervalSeconds int `json:"scan_interval_seconds"`
|
||||
MaxConcurrent int `json:"max_concurrent"`
|
||||
MinServerCount int `json:"min_server_count"`
|
||||
MoveDuringOffHours bool `json:"move_during_off_hours"`
|
||||
OffHoursStart string `json:"off_hours_start"`
|
||||
OffHoursEnd string `json:"off_hours_end"`
|
||||
MinIntervalSeconds int `json:"min_interval_seconds"`
|
||||
}
|
||||
|
||||
// Helper functions for duration conversion
|
||||
func secondsToDuration(seconds int) time.Duration {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
func durationToSeconds(d time.Duration) int {
|
||||
return int(d.Seconds())
|
||||
}
|
||||
|
||||
// formatDurationForUser formats seconds as a user-friendly duration string
|
||||
func formatDurationForUser(seconds int) string {
|
||||
d := secondsToDuration(seconds)
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
if d < time.Hour {
|
||||
return fmt.Sprintf("%.0fm", d.Minutes())
|
||||
}
|
||||
if d < 24*time.Hour {
|
||||
return fmt.Sprintf("%.1fh", d.Hours())
|
||||
}
|
||||
return fmt.Sprintf("%.1fd", d.Hours()/24)
|
||||
}
|
||||
|
||||
// RenderConfigForm renders the configuration form HTML
|
||||
func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML, error) {
|
||||
config := ui.getCurrentBalanceConfig()
|
||||
|
||||
// Build form using the FormBuilder helper
|
||||
form := types.NewFormBuilder()
|
||||
|
||||
// Detection Settings
|
||||
form.AddCheckboxField(
|
||||
"enabled",
|
||||
"Enable Balance Tasks",
|
||||
"Whether balance tasks should be automatically created",
|
||||
config.Enabled,
|
||||
)
|
||||
|
||||
form.AddNumberField(
|
||||
"imbalance_threshold",
|
||||
"Imbalance Threshold (%)",
|
||||
"Trigger balance when storage imbalance exceeds this percentage (0.0-1.0)",
|
||||
config.ImbalanceThreshold,
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddIntervalField("scan_interval", "Scan Interval", "How often to scan for imbalanced volumes", config.ScanIntervalSeconds, true)
|
||||
|
||||
// Scheduling Settings
|
||||
form.AddNumberField(
|
||||
"max_concurrent",
|
||||
"Max Concurrent Tasks",
|
||||
"Maximum number of balance tasks that can run simultaneously",
|
||||
float64(config.MaxConcurrent),
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddNumberField(
|
||||
"min_server_count",
|
||||
"Minimum Server Count",
|
||||
"Only balance when at least this many servers are available",
|
||||
float64(config.MinServerCount),
|
||||
true,
|
||||
)
|
||||
|
||||
// Timing Settings
|
||||
form.AddCheckboxField(
|
||||
"move_during_off_hours",
|
||||
"Restrict to Off-Hours",
|
||||
"Only perform balance operations during off-peak hours",
|
||||
config.MoveDuringOffHours,
|
||||
)
|
||||
|
||||
form.AddTextField(
|
||||
"off_hours_start",
|
||||
"Off-Hours Start Time",
|
||||
"Start time for off-hours window (e.g., 23:00)",
|
||||
config.OffHoursStart,
|
||||
false,
|
||||
)
|
||||
|
||||
form.AddTextField(
|
||||
"off_hours_end",
|
||||
"Off-Hours End Time",
|
||||
"End time for off-hours window (e.g., 06:00)",
|
||||
config.OffHoursEnd,
|
||||
false,
|
||||
)
|
||||
|
||||
// Timing constraints
|
||||
form.AddDurationField("min_interval", "Min Interval", "Minimum time between balance operations", secondsToDuration(config.MinIntervalSeconds), true)
|
||||
|
||||
// Generate organized form sections using Bootstrap components
|
||||
html := `
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-balance-scale me-2"></i>
|
||||
Balance Configuration
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
` + string(form.Build()) + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Performance Considerations
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6 class="alert-heading">Important Considerations:</h6>
|
||||
<p class="mb-2"><strong>Performance:</strong> Volume balancing involves data movement and can impact cluster performance.</p>
|
||||
<p class="mb-2"><strong>Recommendation:</strong> Enable off-hours restriction to minimize impact on production workloads.</p>
|
||||
<p class="mb-0"><strong>Safety:</strong> Requires at least ` + fmt.Sprintf("%d", config.MinServerCount) + ` servers to ensure data safety during moves.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
return template.HTML(html), nil
|
||||
}
|
||||
|
||||
// ParseConfigForm parses form data into configuration
|
||||
func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) {
|
||||
config := &BalanceConfig{}
|
||||
|
||||
// Parse enabled
|
||||
config.Enabled = len(formData["enabled"]) > 0
|
||||
|
||||
// Parse imbalance threshold
|
||||
if values, ok := formData["imbalance_threshold"]; ok && len(values) > 0 {
|
||||
threshold, err := strconv.ParseFloat(values[0], 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid imbalance threshold: %w", err)
|
||||
}
|
||||
if threshold < 0 || threshold > 1 {
|
||||
return nil, fmt.Errorf("imbalance threshold must be between 0.0 and 1.0")
|
||||
}
|
||||
config.ImbalanceThreshold = threshold
|
||||
}
|
||||
|
||||
// Parse scan interval
|
||||
if values, ok := formData["scan_interval_value"]; ok && len(values) > 0 {
|
||||
value, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scan interval value: %w", err)
|
||||
}
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["scan_interval_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse max concurrent
|
||||
if values, ok := formData["max_concurrent"]; ok && len(values) > 0 {
|
||||
maxConcurrent, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid max concurrent: %w", err)
|
||||
}
|
||||
if maxConcurrent < 1 {
|
||||
return nil, fmt.Errorf("max concurrent must be at least 1")
|
||||
}
|
||||
config.MaxConcurrent = maxConcurrent
|
||||
}
|
||||
|
||||
// Parse min server count
|
||||
if values, ok := formData["min_server_count"]; ok && len(values) > 0 {
|
||||
minServerCount, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid min server count: %w", err)
|
||||
}
|
||||
if minServerCount < 2 {
|
||||
return nil, fmt.Errorf("min server count must be at least 2")
|
||||
}
|
||||
config.MinServerCount = minServerCount
|
||||
}
|
||||
|
||||
// Parse off-hours settings
|
||||
config.MoveDuringOffHours = len(formData["move_during_off_hours"]) > 0
|
||||
|
||||
if values, ok := formData["off_hours_start"]; ok && len(values) > 0 {
|
||||
config.OffHoursStart = values[0]
|
||||
}
|
||||
|
||||
if values, ok := formData["off_hours_end"]; ok && len(values) > 0 {
|
||||
config.OffHoursEnd = values[0]
|
||||
}
|
||||
|
||||
// Parse min interval
|
||||
if values, ok := formData["min_interval"]; ok && len(values) > 0 {
|
||||
duration, err := time.ParseDuration(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid min interval: %w", err)
|
||||
}
|
||||
config.MinIntervalSeconds = int(duration.Seconds())
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetCurrentConfig returns the current configuration
|
||||
func (ui *UIProvider) GetCurrentConfig() interface{} {
|
||||
return ui.getCurrentBalanceConfig()
|
||||
}
|
||||
|
||||
// ApplyConfig applies the new configuration
|
||||
func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
balanceConfig, ok := config.(*BalanceConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid config type, expected *BalanceConfig")
|
||||
}
|
||||
|
||||
// Apply to detector
|
||||
if ui.detector != nil {
|
||||
ui.detector.SetEnabled(balanceConfig.Enabled)
|
||||
ui.detector.SetThreshold(balanceConfig.ImbalanceThreshold)
|
||||
ui.detector.SetMinCheckInterval(secondsToDuration(balanceConfig.ScanIntervalSeconds))
|
||||
}
|
||||
|
||||
// Apply to scheduler
|
||||
if ui.scheduler != nil {
|
||||
ui.scheduler.SetEnabled(balanceConfig.Enabled)
|
||||
ui.scheduler.SetMaxConcurrent(balanceConfig.MaxConcurrent)
|
||||
ui.scheduler.SetMinServerCount(balanceConfig.MinServerCount)
|
||||
ui.scheduler.SetMoveDuringOffHours(balanceConfig.MoveDuringOffHours)
|
||||
ui.scheduler.SetOffHoursStart(balanceConfig.OffHoursStart)
|
||||
ui.scheduler.SetOffHoursEnd(balanceConfig.OffHoursEnd)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Applied balance configuration: enabled=%v, threshold=%.1f%%, max_concurrent=%d, min_servers=%d, off_hours=%v",
|
||||
balanceConfig.Enabled, balanceConfig.ImbalanceThreshold*100, balanceConfig.MaxConcurrent,
|
||||
balanceConfig.MinServerCount, balanceConfig.MoveDuringOffHours)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCurrentBalanceConfig gets the current configuration from detector and scheduler
|
||||
func (ui *UIProvider) getCurrentBalanceConfig() *BalanceConfig {
|
||||
config := &BalanceConfig{
|
||||
// Default values (fallback if detectors/schedulers are nil)
|
||||
// Default values from schema (matching task_config_schema.go)
|
||||
Enabled: true,
|
||||
ImbalanceThreshold: 0.1, // 10% imbalance
|
||||
ScanIntervalSeconds: durationToSeconds(4 * time.Hour),
|
||||
MaxConcurrent: 1,
|
||||
ImbalanceThreshold: 0.1, // 10%
|
||||
ScanIntervalSeconds: 6 * 60 * 60, // 6 hours
|
||||
MaxConcurrent: 2,
|
||||
MinServerCount: 3,
|
||||
MoveDuringOffHours: true,
|
||||
OffHoursStart: "23:00",
|
||||
OffHoursEnd: "06:00",
|
||||
MinIntervalSeconds: durationToSeconds(1 * time.Hour),
|
||||
}
|
||||
|
||||
// Get current values from detector
|
||||
@@ -336,14 +73,54 @@ func (ui *UIProvider) getCurrentBalanceConfig() *BalanceConfig {
|
||||
if ui.scheduler != nil {
|
||||
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
|
||||
config.MinServerCount = ui.scheduler.GetMinServerCount()
|
||||
config.MoveDuringOffHours = ui.scheduler.GetMoveDuringOffHours()
|
||||
config.OffHoursStart = ui.scheduler.GetOffHoursStart()
|
||||
config.OffHoursEnd = ui.scheduler.GetOffHoursEnd()
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// ApplyConfig applies the new configuration
|
||||
func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
balanceConfig, ok := config.(*BalanceConfig)
|
||||
if !ok {
|
||||
// Try to get the configuration from the schema-based system
|
||||
schema := tasks.GetBalanceTaskConfigSchema()
|
||||
if schema != nil {
|
||||
// Apply defaults to ensure we have a complete config
|
||||
if err := schema.ApplyDefaults(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use reflection to convert to BalanceConfig - simplified approach
|
||||
if bc, ok := config.(*BalanceConfig); ok {
|
||||
balanceConfig = bc
|
||||
} else {
|
||||
glog.Warningf("Config type conversion failed, using current config")
|
||||
balanceConfig = ui.GetCurrentConfig().(*BalanceConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to detector
|
||||
if ui.detector != nil {
|
||||
ui.detector.SetEnabled(balanceConfig.Enabled)
|
||||
ui.detector.SetThreshold(balanceConfig.ImbalanceThreshold)
|
||||
ui.detector.SetMinCheckInterval(time.Duration(balanceConfig.ScanIntervalSeconds) * time.Second)
|
||||
}
|
||||
|
||||
// Apply to scheduler
|
||||
if ui.scheduler != nil {
|
||||
ui.scheduler.SetEnabled(balanceConfig.Enabled)
|
||||
ui.scheduler.SetMaxConcurrent(balanceConfig.MaxConcurrent)
|
||||
ui.scheduler.SetMinServerCount(balanceConfig.MinServerCount)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Applied balance configuration: enabled=%v, threshold=%.1f%%, max_concurrent=%d, min_servers=%d",
|
||||
balanceConfig.Enabled, balanceConfig.ImbalanceThreshold*100, balanceConfig.MaxConcurrent,
|
||||
balanceConfig.MinServerCount)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterUI registers the balance UI provider with the UI registry
|
||||
func RegisterUI(uiRegistry *types.UIRegistry, detector *BalanceDetector, scheduler *BalanceScheduler) {
|
||||
uiProvider := NewUIProvider(detector, scheduler)
|
||||
@@ -356,13 +133,9 @@ func RegisterUI(uiRegistry *types.UIRegistry, detector *BalanceDetector, schedul
|
||||
func DefaultBalanceConfig() *BalanceConfig {
|
||||
return &BalanceConfig{
|
||||
Enabled: false,
|
||||
ImbalanceThreshold: 0.3,
|
||||
ScanIntervalSeconds: durationToSeconds(4 * time.Hour),
|
||||
MaxConcurrent: 1,
|
||||
ImbalanceThreshold: 0.1, // 10%
|
||||
ScanIntervalSeconds: 6 * 60 * 60, // 6 hours
|
||||
MaxConcurrent: 2,
|
||||
MinServerCount: 3,
|
||||
MoveDuringOffHours: false,
|
||||
OffHoursStart: "22:00",
|
||||
OffHoursEnd: "06:00",
|
||||
MinIntervalSeconds: durationToSeconds(1 * time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package erasure_coding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
@@ -44,252 +42,25 @@ func (ui *UIProvider) GetIcon() string {
|
||||
return "fas fa-shield-alt text-info"
|
||||
}
|
||||
|
||||
// ErasureCodingConfig represents the erasure coding configuration
|
||||
// ErasureCodingConfig represents the erasure coding configuration matching the schema
|
||||
type ErasureCodingConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
QuietForSeconds int `json:"quiet_for_seconds"`
|
||||
FullnessRatio float64 `json:"fullness_ratio"`
|
||||
ScanIntervalSeconds int `json:"scan_interval_seconds"`
|
||||
MaxConcurrent int `json:"max_concurrent"`
|
||||
FullnessRatio float64 `json:"fullness_ratio"`
|
||||
CollectionFilter string `json:"collection_filter"`
|
||||
}
|
||||
|
||||
// Helper functions for duration conversion
|
||||
func secondsToDuration(seconds int) time.Duration {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
func durationToSeconds(d time.Duration) int {
|
||||
return int(d.Seconds())
|
||||
}
|
||||
|
||||
// formatDurationForUser formats seconds as a user-friendly duration string
|
||||
func formatDurationForUser(seconds int) string {
|
||||
d := secondsToDuration(seconds)
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
if d < time.Hour {
|
||||
return fmt.Sprintf("%.0fm", d.Minutes())
|
||||
}
|
||||
if d < 24*time.Hour {
|
||||
return fmt.Sprintf("%.1fh", d.Hours())
|
||||
}
|
||||
return fmt.Sprintf("%.1fd", d.Hours()/24)
|
||||
}
|
||||
|
||||
// RenderConfigForm renders the configuration form HTML
|
||||
func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML, error) {
|
||||
config := ui.getCurrentECConfig()
|
||||
|
||||
// Build form using the FormBuilder helper
|
||||
form := types.NewFormBuilder()
|
||||
|
||||
// Detection Settings
|
||||
form.AddCheckboxField(
|
||||
"enabled",
|
||||
"Enable Erasure Coding Tasks",
|
||||
"Whether erasure coding tasks should be automatically created",
|
||||
config.Enabled,
|
||||
)
|
||||
|
||||
form.AddIntervalField(
|
||||
"quiet_for_seconds",
|
||||
"Quiet For Duration",
|
||||
"Only apply erasure coding to volumes that have not been modified for this duration",
|
||||
config.QuietForSeconds,
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddIntervalField(
|
||||
"scan_interval_seconds",
|
||||
"Scan Interval",
|
||||
"How often to scan for volumes needing erasure coding",
|
||||
config.ScanIntervalSeconds,
|
||||
true,
|
||||
)
|
||||
|
||||
// Scheduling Settings
|
||||
form.AddNumberField(
|
||||
"max_concurrent",
|
||||
"Max Concurrent Tasks",
|
||||
"Maximum number of erasure coding tasks that can run simultaneously",
|
||||
float64(config.MaxConcurrent),
|
||||
true,
|
||||
)
|
||||
|
||||
// Detection Parameters
|
||||
form.AddNumberField(
|
||||
"fullness_ratio",
|
||||
"Fullness Ratio (0.0-1.0)",
|
||||
"Only apply erasure coding to volumes with fullness ratio above this threshold (e.g., 0.90 for 90%)",
|
||||
config.FullnessRatio,
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddTextField(
|
||||
"collection_filter",
|
||||
"Collection Filter",
|
||||
"Only apply erasure coding to volumes in these collections (comma-separated, leave empty for all collections)",
|
||||
config.CollectionFilter,
|
||||
false,
|
||||
)
|
||||
|
||||
// Generate organized form sections using Bootstrap components
|
||||
html := `
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-shield-alt me-2"></i>
|
||||
Erasure Coding Configuration
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
` + string(form.Build()) + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Performance Impact
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h6 class="alert-heading">Important Notes:</h6>
|
||||
<p class="mb-2"><strong>Performance:</strong> Erasure coding is CPU and I/O intensive. Consider running during off-peak hours.</p>
|
||||
<p class="mb-2"><strong>Durability:</strong> With 10+4 configuration, can tolerate up to 4 shard failures.</p>
|
||||
<p class="mb-0"><strong>Configuration:</strong> Use the dropdown to select time units (days, hours, minutes). Fullness ratio should be between 0.0 and 1.0 (e.g., 0.90 for 90%).</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
return template.HTML(html), nil
|
||||
}
|
||||
|
||||
// ParseConfigForm parses form data into configuration
|
||||
func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) {
|
||||
config := ErasureCodingConfig{}
|
||||
|
||||
// Parse enabled
|
||||
config.Enabled = len(formData["enabled"]) > 0
|
||||
|
||||
// Parse quiet for duration
|
||||
if values, ok := formData["quiet_for_seconds_value"]; ok && len(values) > 0 {
|
||||
value, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid quiet for duration value: %w", err)
|
||||
}
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["quiet_for_seconds_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds using the helper function from types package
|
||||
config.QuietForSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse scan interval
|
||||
if values, ok := formData["scan_interval_seconds_value"]; ok && len(values) > 0 {
|
||||
value, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scan interval value: %w", err)
|
||||
}
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["scan_interval_seconds_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse max concurrent
|
||||
if values, ok := formData["max_concurrent"]; ok && len(values) > 0 {
|
||||
maxConcurrent, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid max concurrent: %w", err)
|
||||
}
|
||||
if maxConcurrent < 1 {
|
||||
return nil, fmt.Errorf("max concurrent must be at least 1")
|
||||
}
|
||||
config.MaxConcurrent = maxConcurrent
|
||||
}
|
||||
|
||||
// Parse fullness ratio
|
||||
if values, ok := formData["fullness_ratio"]; ok && len(values) > 0 {
|
||||
fullnessRatio, err := strconv.ParseFloat(values[0], 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid fullness ratio: %w", err)
|
||||
}
|
||||
if fullnessRatio < 0 || fullnessRatio > 1 {
|
||||
return nil, fmt.Errorf("fullness ratio must be between 0.0 and 1.0")
|
||||
}
|
||||
config.FullnessRatio = fullnessRatio
|
||||
}
|
||||
|
||||
// Parse collection filter
|
||||
if values, ok := formData["collection_filter"]; ok && len(values) > 0 {
|
||||
config.CollectionFilter = values[0]
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetCurrentConfig returns the current configuration
|
||||
func (ui *UIProvider) GetCurrentConfig() interface{} {
|
||||
return ui.getCurrentECConfig()
|
||||
}
|
||||
|
||||
// ApplyConfig applies the new configuration
|
||||
func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
ecConfig, ok := config.(ErasureCodingConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid config type, expected ErasureCodingConfig")
|
||||
}
|
||||
|
||||
// Apply to detector
|
||||
if ui.detector != nil {
|
||||
ui.detector.SetEnabled(ecConfig.Enabled)
|
||||
ui.detector.SetQuietForSeconds(ecConfig.QuietForSeconds)
|
||||
ui.detector.SetFullnessRatio(ecConfig.FullnessRatio)
|
||||
ui.detector.SetCollectionFilter(ecConfig.CollectionFilter)
|
||||
ui.detector.SetScanInterval(secondsToDuration(ecConfig.ScanIntervalSeconds))
|
||||
}
|
||||
|
||||
// Apply to scheduler
|
||||
if ui.scheduler != nil {
|
||||
ui.scheduler.SetEnabled(ecConfig.Enabled)
|
||||
ui.scheduler.SetMaxConcurrent(ecConfig.MaxConcurrent)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Applied erasure coding configuration: enabled=%v, quiet_for=%v seconds, max_concurrent=%d, fullness_ratio=%f, collection_filter=%s, shards=10+4",
|
||||
ecConfig.Enabled, ecConfig.QuietForSeconds, ecConfig.MaxConcurrent, ecConfig.FullnessRatio, ecConfig.CollectionFilter)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCurrentECConfig gets the current configuration from detector and scheduler
|
||||
func (ui *UIProvider) getCurrentECConfig() ErasureCodingConfig {
|
||||
config := ErasureCodingConfig{
|
||||
// Default values (fallback if detectors/schedulers are nil)
|
||||
// Default values from schema (matching task_config_schema.go)
|
||||
Enabled: true,
|
||||
QuietForSeconds: 24 * 3600, // Default to 24 hours in seconds
|
||||
ScanIntervalSeconds: 2 * 3600, // 2 hours in seconds
|
||||
QuietForSeconds: 7 * 24 * 60 * 60, // 7 days
|
||||
ScanIntervalSeconds: 12 * 60 * 60, // 12 hours
|
||||
MaxConcurrent: 1,
|
||||
FullnessRatio: 0.90, // Default fullness ratio
|
||||
FullnessRatio: 0.9, // 90%
|
||||
CollectionFilter: "",
|
||||
}
|
||||
|
||||
@@ -299,7 +70,7 @@ func (ui *UIProvider) getCurrentECConfig() ErasureCodingConfig {
|
||||
config.QuietForSeconds = ui.detector.GetQuietForSeconds()
|
||||
config.FullnessRatio = ui.detector.GetFullnessRatio()
|
||||
config.CollectionFilter = ui.detector.GetCollectionFilter()
|
||||
config.ScanIntervalSeconds = durationToSeconds(ui.detector.ScanInterval())
|
||||
config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds())
|
||||
}
|
||||
|
||||
// Get current values from scheduler
|
||||
@@ -310,6 +81,49 @@ func (ui *UIProvider) getCurrentECConfig() ErasureCodingConfig {
|
||||
return config
|
||||
}
|
||||
|
||||
// ApplyConfig applies the new configuration
|
||||
func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
ecConfig, ok := config.(ErasureCodingConfig)
|
||||
if !ok {
|
||||
// Try to get the configuration from the schema-based system
|
||||
schema := tasks.GetErasureCodingTaskConfigSchema()
|
||||
if schema != nil {
|
||||
// Apply defaults to ensure we have a complete config
|
||||
if err := schema.ApplyDefaults(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use reflection to convert to ErasureCodingConfig - simplified approach
|
||||
if ec, ok := config.(ErasureCodingConfig); ok {
|
||||
ecConfig = ec
|
||||
} else {
|
||||
glog.Warningf("Config type conversion failed, using current config")
|
||||
ecConfig = ui.GetCurrentConfig().(ErasureCodingConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to detector
|
||||
if ui.detector != nil {
|
||||
ui.detector.SetEnabled(ecConfig.Enabled)
|
||||
ui.detector.SetQuietForSeconds(ecConfig.QuietForSeconds)
|
||||
ui.detector.SetFullnessRatio(ecConfig.FullnessRatio)
|
||||
ui.detector.SetCollectionFilter(ecConfig.CollectionFilter)
|
||||
ui.detector.SetScanInterval(time.Duration(ecConfig.ScanIntervalSeconds) * time.Second)
|
||||
}
|
||||
|
||||
// Apply to scheduler
|
||||
if ui.scheduler != nil {
|
||||
ui.scheduler.SetEnabled(ecConfig.Enabled)
|
||||
ui.scheduler.SetMaxConcurrent(ecConfig.MaxConcurrent)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Applied erasure coding configuration: enabled=%v, quiet_for=%v seconds, max_concurrent=%d, fullness_ratio=%f, collection_filter=%s",
|
||||
ecConfig.Enabled, ecConfig.QuietForSeconds, ecConfig.MaxConcurrent, ecConfig.FullnessRatio, ecConfig.CollectionFilter)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterUI registers the erasure coding UI provider with the UI registry
|
||||
func RegisterUI(uiRegistry *types.UIRegistry, detector *EcDetector, scheduler *Scheduler) {
|
||||
uiProvider := NewUIProvider(detector, scheduler)
|
||||
|
||||
+54
-245
@@ -1,12 +1,10 @@
|
||||
package vacuum
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
|
||||
"github.com/seaweedfs/seaweedfs/weed/worker/types"
|
||||
)
|
||||
|
||||
@@ -44,7 +42,7 @@ func (ui *UIProvider) GetIcon() string {
|
||||
return "fas fa-broom text-primary"
|
||||
}
|
||||
|
||||
// VacuumConfig represents the vacuum configuration
|
||||
// VacuumConfig represents the vacuum configuration matching the schema
|
||||
type VacuumConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
GarbageThreshold float64 `json:"garbage_threshold"`
|
||||
@@ -54,267 +52,78 @@ type VacuumConfig struct {
|
||||
MinIntervalSeconds int `json:"min_interval_seconds"`
|
||||
}
|
||||
|
||||
// Helper functions for duration conversion
|
||||
func secondsToDuration(seconds int) time.Duration {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
func durationToSeconds(d time.Duration) int {
|
||||
return int(d.Seconds())
|
||||
}
|
||||
|
||||
// formatDurationForUser formats seconds as a user-friendly duration string
|
||||
func formatDurationForUser(seconds int) string {
|
||||
d := secondsToDuration(seconds)
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
if d < time.Hour {
|
||||
return fmt.Sprintf("%.0fm", d.Minutes())
|
||||
}
|
||||
if d < 24*time.Hour {
|
||||
return fmt.Sprintf("%.1fh", d.Hours())
|
||||
}
|
||||
return fmt.Sprintf("%.1fd", d.Hours()/24)
|
||||
}
|
||||
|
||||
// RenderConfigForm renders the configuration form HTML
|
||||
func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML, error) {
|
||||
config := ui.getCurrentVacuumConfig()
|
||||
|
||||
// Build form using the FormBuilder helper
|
||||
form := types.NewFormBuilder()
|
||||
|
||||
// Detection Settings
|
||||
form.AddCheckboxField(
|
||||
"enabled",
|
||||
"Enable Vacuum Tasks",
|
||||
"Whether vacuum tasks should be automatically created",
|
||||
config.Enabled,
|
||||
)
|
||||
|
||||
form.AddNumberField(
|
||||
"garbage_threshold",
|
||||
"Garbage Percentage Threshold",
|
||||
"Trigger vacuum when garbage ratio exceeds this percentage (0-100)",
|
||||
config.GarbageThreshold*100, // Convert 0.0-1.0 to 0-100 for display
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddIntervalField(
|
||||
"scan_interval_seconds",
|
||||
"Scan Interval",
|
||||
"How often to scan for volumes needing vacuum",
|
||||
config.ScanIntervalSeconds,
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddIntervalField(
|
||||
"min_volume_age_seconds",
|
||||
"Minimum Volume Age",
|
||||
"Only vacuum volumes older than this duration",
|
||||
config.MinVolumeAgeSeconds,
|
||||
true,
|
||||
)
|
||||
|
||||
// Scheduling Settings
|
||||
form.AddNumberField(
|
||||
"max_concurrent",
|
||||
"Max Concurrent Tasks",
|
||||
"Maximum number of vacuum tasks that can run simultaneously",
|
||||
float64(config.MaxConcurrent),
|
||||
true,
|
||||
)
|
||||
|
||||
form.AddIntervalField(
|
||||
"min_interval_seconds",
|
||||
"Minimum Interval",
|
||||
"Minimum time between vacuum operations on the same volume",
|
||||
config.MinIntervalSeconds,
|
||||
true,
|
||||
)
|
||||
|
||||
// Generate organized form sections using Bootstrap components
|
||||
html := `
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-search me-2"></i>
|
||||
Detection Settings
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
` + string(form.Build()) + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function resetForm() {
|
||||
if (confirm('Reset all vacuum settings to defaults?')) {
|
||||
// Reset to default values
|
||||
document.querySelector('input[name="enabled"]').checked = true;
|
||||
document.querySelector('input[name="garbage_threshold"]').value = '30';
|
||||
document.querySelector('input[name="scan_interval_seconds_value"]').value = '30';
|
||||
document.querySelector('select[name="scan_interval_seconds_unit"]').value = 'minute';
|
||||
document.querySelector('input[name="min_volume_age_seconds_value"]').value = '1';
|
||||
document.querySelector('select[name="min_volume_age_seconds_unit"]').value = 'hour';
|
||||
document.querySelector('input[name="max_concurrent"]').value = '2';
|
||||
document.querySelector('input[name="min_interval_seconds_value"]').value = '6';
|
||||
document.querySelector('select[name="min_interval_seconds_unit"]').value = 'hour';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
`
|
||||
|
||||
return template.HTML(html), nil
|
||||
}
|
||||
|
||||
// ParseConfigForm parses form data into configuration
|
||||
func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) {
|
||||
config := &VacuumConfig{}
|
||||
|
||||
// Parse enabled checkbox
|
||||
config.Enabled = len(formData["enabled"]) > 0 && formData["enabled"][0] == "on"
|
||||
|
||||
// Parse garbage threshold (convert from 0-100 to 0.0-1.0)
|
||||
if thresholdStr := formData["garbage_threshold"]; len(thresholdStr) > 0 {
|
||||
if threshold, err := strconv.ParseFloat(thresholdStr[0], 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid garbage percentage threshold: %w", err)
|
||||
} else if threshold < 0 || threshold > 100 {
|
||||
return nil, fmt.Errorf("garbage percentage threshold must be between 0 and 100")
|
||||
} else {
|
||||
config.GarbageThreshold = threshold / 100.0 // Convert percentage to decimal
|
||||
}
|
||||
}
|
||||
|
||||
// Parse scan interval
|
||||
if values, ok := formData["scan_interval_seconds_value"]; ok && len(values) > 0 {
|
||||
value, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scan interval value: %w", err)
|
||||
}
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["scan_interval_seconds_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds using the helper function from types package
|
||||
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse min volume age
|
||||
if values, ok := formData["min_volume_age_seconds_value"]; ok && len(values) > 0 {
|
||||
value, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid min volume age value: %w", err)
|
||||
}
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["min_volume_age_seconds_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
config.MinVolumeAgeSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
// Parse max concurrent
|
||||
if concurrentStr := formData["max_concurrent"]; len(concurrentStr) > 0 {
|
||||
if concurrent, err := strconv.Atoi(concurrentStr[0]); err != nil {
|
||||
return nil, fmt.Errorf("invalid max concurrent: %w", err)
|
||||
} else if concurrent < 1 {
|
||||
return nil, fmt.Errorf("max concurrent must be at least 1")
|
||||
} else {
|
||||
config.MaxConcurrent = concurrent
|
||||
}
|
||||
}
|
||||
|
||||
// Parse min interval
|
||||
if values, ok := formData["min_interval_seconds_value"]; ok && len(values) > 0 {
|
||||
value, err := strconv.Atoi(values[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid min interval value: %w", err)
|
||||
}
|
||||
|
||||
unit := "minute" // default
|
||||
if units, ok := formData["min_interval_seconds_unit"]; ok && len(units) > 0 {
|
||||
unit = units[0]
|
||||
}
|
||||
|
||||
// Convert to seconds
|
||||
config.MinIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetCurrentConfig returns the current configuration
|
||||
func (ui *UIProvider) GetCurrentConfig() interface{} {
|
||||
return ui.getCurrentVacuumConfig()
|
||||
}
|
||||
|
||||
// ApplyConfig applies the new configuration
|
||||
func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
vacuumConfig, ok := config.(*VacuumConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid config type, expected *VacuumConfig")
|
||||
}
|
||||
|
||||
// Apply to detector
|
||||
if ui.detector != nil {
|
||||
ui.detector.SetEnabled(vacuumConfig.Enabled)
|
||||
ui.detector.SetGarbageThreshold(vacuumConfig.GarbageThreshold)
|
||||
ui.detector.SetScanInterval(secondsToDuration(vacuumConfig.ScanIntervalSeconds))
|
||||
ui.detector.SetMinVolumeAge(secondsToDuration(vacuumConfig.MinVolumeAgeSeconds))
|
||||
}
|
||||
|
||||
// Apply to scheduler
|
||||
if ui.scheduler != nil {
|
||||
ui.scheduler.SetEnabled(vacuumConfig.Enabled)
|
||||
ui.scheduler.SetMaxConcurrent(vacuumConfig.MaxConcurrent)
|
||||
ui.scheduler.SetMinInterval(secondsToDuration(vacuumConfig.MinIntervalSeconds))
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Applied vacuum configuration: enabled=%v, threshold=%.1f%%, scan_interval=%s, max_concurrent=%d",
|
||||
vacuumConfig.Enabled, vacuumConfig.GarbageThreshold*100, formatDurationForUser(vacuumConfig.ScanIntervalSeconds), vacuumConfig.MaxConcurrent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCurrentVacuumConfig gets the current configuration from detector and scheduler
|
||||
func (ui *UIProvider) getCurrentVacuumConfig() *VacuumConfig {
|
||||
config := &VacuumConfig{
|
||||
// Default values (fallback if detectors/schedulers are nil)
|
||||
// Default values from schema (matching task_config_schema.go)
|
||||
Enabled: true,
|
||||
GarbageThreshold: 0.3,
|
||||
ScanIntervalSeconds: 30 * 60,
|
||||
MinVolumeAgeSeconds: 1 * 60 * 60,
|
||||
GarbageThreshold: 0.3, // 30%
|
||||
ScanIntervalSeconds: 2 * 60 * 60, // 2 hours
|
||||
MaxConcurrent: 2,
|
||||
MinIntervalSeconds: 6 * 60 * 60,
|
||||
MinVolumeAgeSeconds: 24 * 60 * 60, // 24 hours
|
||||
MinIntervalSeconds: 7 * 24 * 60 * 60, // 7 days
|
||||
}
|
||||
|
||||
// Get current values from detector
|
||||
if ui.detector != nil {
|
||||
config.Enabled = ui.detector.IsEnabled()
|
||||
config.GarbageThreshold = ui.detector.GetGarbageThreshold()
|
||||
config.ScanIntervalSeconds = durationToSeconds(ui.detector.ScanInterval())
|
||||
config.MinVolumeAgeSeconds = durationToSeconds(ui.detector.GetMinVolumeAge())
|
||||
config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds())
|
||||
config.MinVolumeAgeSeconds = int(ui.detector.GetMinVolumeAge().Seconds())
|
||||
}
|
||||
|
||||
// Get current values from scheduler
|
||||
if ui.scheduler != nil {
|
||||
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
|
||||
config.MinIntervalSeconds = durationToSeconds(ui.scheduler.GetMinInterval())
|
||||
config.MinIntervalSeconds = int(ui.scheduler.GetMinInterval().Seconds())
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// ApplyConfig applies the new configuration
|
||||
func (ui *UIProvider) ApplyConfig(config interface{}) error {
|
||||
vacuumConfig, ok := config.(*VacuumConfig)
|
||||
if !ok {
|
||||
// Try to get the configuration from the schema-based system
|
||||
schema := tasks.GetVacuumTaskConfigSchema()
|
||||
if schema != nil {
|
||||
// Apply defaults to ensure we have a complete config
|
||||
if err := schema.ApplyDefaults(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use reflection to convert to VacuumConfig - simplified approach
|
||||
if vc, ok := config.(*VacuumConfig); ok {
|
||||
vacuumConfig = vc
|
||||
} else {
|
||||
glog.Warningf("Config type conversion failed, using current config")
|
||||
vacuumConfig = ui.GetCurrentConfig().(*VacuumConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to detector
|
||||
if ui.detector != nil {
|
||||
ui.detector.SetEnabled(vacuumConfig.Enabled)
|
||||
ui.detector.SetGarbageThreshold(vacuumConfig.GarbageThreshold)
|
||||
ui.detector.SetScanInterval(time.Duration(vacuumConfig.ScanIntervalSeconds) * time.Second)
|
||||
ui.detector.SetMinVolumeAge(time.Duration(vacuumConfig.MinVolumeAgeSeconds) * time.Second)
|
||||
}
|
||||
|
||||
// Apply to scheduler
|
||||
if ui.scheduler != nil {
|
||||
ui.scheduler.SetEnabled(vacuumConfig.Enabled)
|
||||
ui.scheduler.SetMaxConcurrent(vacuumConfig.MaxConcurrent)
|
||||
ui.scheduler.SetMinInterval(time.Duration(vacuumConfig.MinIntervalSeconds) * time.Second)
|
||||
}
|
||||
|
||||
glog.V(1).Infof("Applied vacuum configuration: enabled=%v, threshold=%.1f%%, scan_interval=%ds, max_concurrent=%d",
|
||||
vacuumConfig.Enabled, vacuumConfig.GarbageThreshold*100, vacuumConfig.ScanIntervalSeconds, vacuumConfig.MaxConcurrent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterUI registers the vacuum UI provider with the UI registry
|
||||
func RegisterUI(uiRegistry *types.UIRegistry, detector *VacuumDetector, scheduler *VacuumScheduler) {
|
||||
uiProvider := NewUIProvider(detector, scheduler)
|
||||
@@ -323,7 +132,7 @@ func RegisterUI(uiRegistry *types.UIRegistry, detector *VacuumDetector, schedule
|
||||
glog.V(1).Infof("✅ Registered vacuum task UI provider")
|
||||
}
|
||||
|
||||
// Example: How to get the UI provider for external use
|
||||
// GetUIProvider returns the UI provider for external use
|
||||
func GetUIProvider(uiRegistry *types.UIRegistry) *UIProvider {
|
||||
provider := uiRegistry.GetProvider(types.TaskTypeVacuum)
|
||||
if provider == nil {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -41,6 +39,7 @@ func IntervalValueUnitToSeconds(value int, unit string) int {
|
||||
}
|
||||
|
||||
// TaskUIProvider defines how tasks provide their configuration UI
|
||||
// This interface is simplified to work with schema-driven configuration
|
||||
type TaskUIProvider interface {
|
||||
// GetTaskType returns the task type
|
||||
GetTaskType() TaskType
|
||||
@@ -54,12 +53,6 @@ type TaskUIProvider interface {
|
||||
// GetIcon returns the icon CSS class or HTML for this task type
|
||||
GetIcon() string
|
||||
|
||||
// RenderConfigForm renders the configuration form HTML
|
||||
RenderConfigForm(currentConfig interface{}) (template.HTML, error)
|
||||
|
||||
// ParseConfigForm parses form data into configuration
|
||||
ParseConfigForm(formData map[string][]string) (interface{}, error)
|
||||
|
||||
// GetCurrentConfig returns the current configuration
|
||||
GetCurrentConfig() interface{}
|
||||
|
||||
@@ -121,246 +114,10 @@ type TaskListData struct {
|
||||
}
|
||||
|
||||
type TaskDetailsData struct {
|
||||
Task *Task `json:"task"`
|
||||
TaskType TaskType `json:"task_type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
Stats *TaskStats `json:"stats"`
|
||||
ConfigForm template.HTML `json:"config_form"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
// Common form field types for simple form building
|
||||
type FormField struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"` // text, number, checkbox, select, duration, interval
|
||||
Value interface{} `json:"value"`
|
||||
Description string `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
Options []FormOption `json:"options,omitempty"` // For select fields
|
||||
// Interval-specific fields
|
||||
IntervalValue int `json:"interval_value,omitempty"` // Numeric value for interval
|
||||
IntervalUnit string `json:"interval_unit,omitempty"` // Unit for interval (day, hour, minute)
|
||||
}
|
||||
|
||||
type FormOption struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// Helper for building forms in code
|
||||
type FormBuilder struct {
|
||||
fields []FormField
|
||||
}
|
||||
|
||||
// NewFormBuilder creates a new form builder
|
||||
func NewFormBuilder() *FormBuilder {
|
||||
return &FormBuilder{
|
||||
fields: make([]FormField, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// AddTextField adds a text input field
|
||||
func (fb *FormBuilder) AddTextField(name, label, description string, value string, required bool) *FormBuilder {
|
||||
fb.fields = append(fb.fields, FormField{
|
||||
Name: name,
|
||||
Label: label,
|
||||
Type: "text",
|
||||
Value: value,
|
||||
Description: description,
|
||||
Required: required,
|
||||
})
|
||||
return fb
|
||||
}
|
||||
|
||||
// AddNumberField adds a number input field
|
||||
func (fb *FormBuilder) AddNumberField(name, label, description string, value float64, required bool) *FormBuilder {
|
||||
fb.fields = append(fb.fields, FormField{
|
||||
Name: name,
|
||||
Label: label,
|
||||
Type: "number",
|
||||
Value: value,
|
||||
Description: description,
|
||||
Required: required,
|
||||
})
|
||||
return fb
|
||||
}
|
||||
|
||||
// AddCheckboxField adds a checkbox field
|
||||
func (fb *FormBuilder) AddCheckboxField(name, label, description string, value bool) *FormBuilder {
|
||||
fb.fields = append(fb.fields, FormField{
|
||||
Name: name,
|
||||
Label: label,
|
||||
Type: "checkbox",
|
||||
Value: value,
|
||||
Description: description,
|
||||
Required: false,
|
||||
})
|
||||
return fb
|
||||
}
|
||||
|
||||
// AddSelectField adds a select dropdown field
|
||||
func (fb *FormBuilder) AddSelectField(name, label, description string, value string, options []FormOption, required bool) *FormBuilder {
|
||||
fb.fields = append(fb.fields, FormField{
|
||||
Name: name,
|
||||
Label: label,
|
||||
Type: "select",
|
||||
Value: value,
|
||||
Description: description,
|
||||
Required: required,
|
||||
Options: options,
|
||||
})
|
||||
return fb
|
||||
}
|
||||
|
||||
// AddDurationField adds a duration input field
|
||||
func (fb *FormBuilder) AddDurationField(name, label, description string, value time.Duration, required bool) *FormBuilder {
|
||||
fb.fields = append(fb.fields, FormField{
|
||||
Name: name,
|
||||
Label: label,
|
||||
Type: "duration",
|
||||
Value: value.String(),
|
||||
Description: description,
|
||||
Required: required,
|
||||
})
|
||||
return fb
|
||||
}
|
||||
|
||||
// AddIntervalField adds an interval field with value and unit dropdown
|
||||
func (fb *FormBuilder) AddIntervalField(name, label, description string, totalSeconds int, required bool) *FormBuilder {
|
||||
// Convert seconds to the most appropriate unit
|
||||
value, unit := secondsToIntervalValueUnit(totalSeconds)
|
||||
|
||||
fb.fields = append(fb.fields, FormField{
|
||||
Name: name,
|
||||
Label: label,
|
||||
Type: "interval",
|
||||
Description: description,
|
||||
Required: required,
|
||||
IntervalValue: value,
|
||||
IntervalUnit: unit,
|
||||
})
|
||||
return fb
|
||||
}
|
||||
|
||||
// Build generates the HTML form fields with Bootstrap styling
|
||||
func (fb *FormBuilder) Build() template.HTML {
|
||||
html := ""
|
||||
|
||||
for _, field := range fb.fields {
|
||||
html += fb.renderField(field)
|
||||
}
|
||||
|
||||
return template.HTML(html)
|
||||
}
|
||||
|
||||
// renderField renders a single form field with Bootstrap classes
|
||||
func (fb *FormBuilder) renderField(field FormField) string {
|
||||
html := "<div class=\"mb-3\">\n"
|
||||
|
||||
// Special handling for checkbox fields
|
||||
if field.Type == "checkbox" {
|
||||
checked := ""
|
||||
if field.Value.(bool) {
|
||||
checked = " checked"
|
||||
}
|
||||
html += " <div class=\"form-check\">\n"
|
||||
html += " <input type=\"checkbox\" class=\"form-check-input\" id=\"" + field.Name + "\" name=\"" + field.Name + "\"" + checked + ">\n"
|
||||
html += " <label class=\"form-check-label\" for=\"" + field.Name + "\">" + field.Label + "</label>\n"
|
||||
html += " </div>\n"
|
||||
// Description for checkbox
|
||||
if field.Description != "" {
|
||||
html += " <div class=\"form-text text-muted\">" + field.Description + "</div>\n"
|
||||
}
|
||||
html += "</div>\n"
|
||||
return html
|
||||
}
|
||||
|
||||
// Label for non-checkbox fields
|
||||
required := ""
|
||||
if field.Required {
|
||||
required = " <span class=\"text-danger\">*</span>"
|
||||
}
|
||||
html += " <label for=\"" + field.Name + "\" class=\"form-label\">" + field.Label + required + "</label>\n"
|
||||
|
||||
// Input based on type
|
||||
switch field.Type {
|
||||
case "text":
|
||||
html += " <input type=\"text\" class=\"form-control\" id=\"" + field.Name + "\" name=\"" + field.Name + "\" value=\"" + field.Value.(string) + "\""
|
||||
if field.Required {
|
||||
html += " required"
|
||||
}
|
||||
html += ">\n"
|
||||
|
||||
case "number":
|
||||
html += " <input type=\"number\" class=\"form-control\" id=\"" + field.Name + "\" name=\"" + field.Name + "\" step=\"any\" value=\"" +
|
||||
fmt.Sprintf("%v", field.Value) + "\""
|
||||
if field.Required {
|
||||
html += " required"
|
||||
}
|
||||
html += ">\n"
|
||||
|
||||
case "select":
|
||||
html += " <select class=\"form-select\" id=\"" + field.Name + "\" name=\"" + field.Name + "\""
|
||||
if field.Required {
|
||||
html += " required"
|
||||
}
|
||||
html += ">\n"
|
||||
for _, option := range field.Options {
|
||||
selected := ""
|
||||
if option.Value == field.Value.(string) {
|
||||
selected = " selected"
|
||||
}
|
||||
html += " <option value=\"" + option.Value + "\"" + selected + ">" + option.Label + "</option>\n"
|
||||
}
|
||||
html += " </select>\n"
|
||||
|
||||
case "duration":
|
||||
html += " <input type=\"text\" class=\"form-control\" id=\"" + field.Name + "\" name=\"" + field.Name + "\" value=\"" + field.Value.(string) +
|
||||
"\" placeholder=\"e.g., 30m, 2h, 24h\""
|
||||
if field.Required {
|
||||
html += " required"
|
||||
}
|
||||
html += ">\n"
|
||||
|
||||
case "interval":
|
||||
// Create input group with number input and unit select
|
||||
html += " <div class=\"input-group\">\n"
|
||||
html += " <input type=\"number\" class=\"form-control\" id=\"" + field.Name + "_value\" name=\"" + field.Name + "_value\" min=\"0\" value=\"" +
|
||||
fmt.Sprintf("%d", field.IntervalValue) + "\""
|
||||
if field.Required {
|
||||
html += " required"
|
||||
}
|
||||
html += ">\n"
|
||||
html += " <select class=\"form-select\" id=\"" + field.Name + "_unit\" name=\"" + field.Name + "_unit\" style=\"max-width: 120px;\""
|
||||
if field.Required {
|
||||
html += " required"
|
||||
}
|
||||
html += ">\n"
|
||||
|
||||
// Add unit options
|
||||
units := []struct{ Value, Label string }{
|
||||
{"minute", "Minutes"},
|
||||
{"hour", "Hours"},
|
||||
{"day", "Days"},
|
||||
}
|
||||
for _, unit := range units {
|
||||
selected := ""
|
||||
if unit.Value == field.IntervalUnit {
|
||||
selected = " selected"
|
||||
}
|
||||
html += " <option value=\"" + unit.Value + "\"" + selected + ">" + unit.Label + "</option>\n"
|
||||
}
|
||||
html += " </select>\n"
|
||||
html += " </div>\n"
|
||||
}
|
||||
|
||||
// Description for non-checkbox fields
|
||||
if field.Description != "" {
|
||||
html += " <div class=\"form-text text-muted\">" + field.Description + "</div>\n"
|
||||
}
|
||||
|
||||
html += "</div>\n"
|
||||
return html
|
||||
Task *Task `json:"task"`
|
||||
TaskType TaskType `json:"task_type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
Stats *TaskStats `json:"stats"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user