diff --git a/weed/admin/handlers/maintenance_handlers.go b/weed/admin/handlers/maintenance_handlers.go
index 137b6a148..8b5c25577 100644
--- a/weed/admin/handlers/maintenance_handlers.go
+++ b/weed/admin/handlers/maintenance_handlers.go
@@ -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
diff --git a/weed/worker/tasks/balance/ui.go b/weed/worker/tasks/balance/ui.go
index b4d04ee8f..e7c8496f2 100644
--- a/weed/worker/tasks/balance/ui.go
+++ b/weed/worker/tasks/balance/ui.go
@@ -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,253 +42,69 @@ 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 := `
-
-
-
-
-
-
- Balance Configuration
-
-
-
-` + string(form.Build()) + `
-
-
-
-
-
-
-
-
-
-
-
- Performance Considerations
-
-
-
-
-
Important Considerations:
-
Performance: Volume balancing involves data movement and can impact cluster performance.
-
Recommendation: Enable off-hours restriction to minimize impact on production workloads.
-
Safety: Requires at least ` + fmt.Sprintf("%d", config.MinServerCount) + ` servers to ensure data safety during moves.
-
-
-
-
-
`
-
- 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]
+// GetCurrentConfig returns the current configuration
+func (ui *UIProvider) GetCurrentConfig() interface{} {
+ config := &BalanceConfig{
+ // Default values from schema (matching task_config_schema.go)
+ Enabled: true,
+ ImbalanceThreshold: 0.1, // 10%
+ ScanIntervalSeconds: 6 * 60 * 60, // 6 hours
+ MaxConcurrent: 2,
+ MinServerCount: 3,
}
- if values, ok := formData["off_hours_end"]; ok && len(values) > 0 {
- config.OffHoursEnd = values[0]
+ // Get current values from detector
+ if ui.detector != nil {
+ config.Enabled = ui.detector.IsEnabled()
+ config.ImbalanceThreshold = ui.detector.GetThreshold()
+ config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds())
}
- // 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())
+ // Get current values from scheduler
+ if ui.scheduler != nil {
+ config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
+ config.MinServerCount = ui.scheduler.GetMinServerCount()
}
- return config, nil
-}
-
-// GetCurrentConfig returns the current configuration
-func (ui *UIProvider) GetCurrentConfig() interface{} {
- return ui.getCurrentBalanceConfig()
+ return config
}
// 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")
+ // 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(secondsToDuration(balanceConfig.ScanIntervalSeconds))
+ ui.detector.SetMinCheckInterval(time.Duration(balanceConfig.ScanIntervalSeconds) * time.Second)
}
// Apply to scheduler
@@ -298,52 +112,15 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
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",
+ 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, balanceConfig.MoveDuringOffHours)
+ balanceConfig.MinServerCount)
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)
- Enabled: true,
- ImbalanceThreshold: 0.1, // 10% imbalance
- ScanIntervalSeconds: durationToSeconds(4 * time.Hour),
- MaxConcurrent: 1,
- MinServerCount: 3,
- MoveDuringOffHours: true,
- OffHoursStart: "23:00",
- OffHoursEnd: "06:00",
- MinIntervalSeconds: durationToSeconds(1 * time.Hour),
- }
-
- // Get current values from detector
- if ui.detector != nil {
- config.Enabled = ui.detector.IsEnabled()
- config.ImbalanceThreshold = ui.detector.GetThreshold()
- config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds())
- }
-
- // Get current values from scheduler
- 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
-}
-
// 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),
}
}
diff --git a/weed/worker/tasks/erasure_coding/ui.go b/weed/worker/tasks/erasure_coding/ui.go
index c61c65628..d9a10b0b2 100644
--- a/weed/worker/tasks/erasure_coding/ui.go
+++ b/weed/worker/tasks/erasure_coding/ui.go
@@ -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,220 +42,65 @@ 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 := `
-
-
-
-
-
-
- Erasure Coding Configuration
-
-
-
-` + string(form.Build()) + `
-
-
-
-
-
-
-
-
-
-
-
- Performance Impact
-
-
-
-
-
Important Notes:
-
Performance: Erasure coding is CPU and I/O intensive. Consider running during off-peak hours.
-
Durability: With 10+4 configuration, can tolerate up to 4 shard failures.
-
Configuration: 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%).
-
-
-
-
-
`
-
- 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
+// GetCurrentConfig returns the current configuration
+func (ui *UIProvider) GetCurrentConfig() interface{} {
+ config := ErasureCodingConfig{
+ // Default values from schema (matching task_config_schema.go)
+ Enabled: true,
+ QuietForSeconds: 7 * 24 * 60 * 60, // 7 days
+ ScanIntervalSeconds: 12 * 60 * 60, // 12 hours
+ MaxConcurrent: 1,
+ FullnessRatio: 0.9, // 90%
+ CollectionFilter: "",
}
- // 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
+ // Get current values from detector
+ if ui.detector != nil {
+ config.Enabled = ui.detector.IsEnabled()
+ config.QuietForSeconds = ui.detector.GetQuietForSeconds()
+ config.FullnessRatio = ui.detector.GetFullnessRatio()
+ config.CollectionFilter = ui.detector.GetCollectionFilter()
+ config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds())
}
- // Parse collection filter
- if values, ok := formData["collection_filter"]; ok && len(values) > 0 {
- config.CollectionFilter = values[0]
+ // Get current values from scheduler
+ if ui.scheduler != nil {
+ config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
}
- return config, nil
-}
-
-// GetCurrentConfig returns the current configuration
-func (ui *UIProvider) GetCurrentConfig() interface{} {
- return ui.getCurrentECConfig()
+ return config
}
// 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")
+ // 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
@@ -266,7 +109,7 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
ui.detector.SetQuietForSeconds(ecConfig.QuietForSeconds)
ui.detector.SetFullnessRatio(ecConfig.FullnessRatio)
ui.detector.SetCollectionFilter(ecConfig.CollectionFilter)
- ui.detector.SetScanInterval(secondsToDuration(ecConfig.ScanIntervalSeconds))
+ ui.detector.SetScanInterval(time.Duration(ecConfig.ScanIntervalSeconds) * time.Second)
}
// Apply to scheduler
@@ -275,41 +118,12 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
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",
+ 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
}
-// getCurrentECConfig gets the current configuration from detector and scheduler
-func (ui *UIProvider) getCurrentECConfig() ErasureCodingConfig {
- config := ErasureCodingConfig{
- // Default values (fallback if detectors/schedulers are nil)
- Enabled: true,
- QuietForSeconds: 24 * 3600, // Default to 24 hours in seconds
- ScanIntervalSeconds: 2 * 3600, // 2 hours in seconds
- MaxConcurrent: 1,
- FullnessRatio: 0.90, // Default fullness ratio
- CollectionFilter: "",
- }
-
- // Get current values from detector
- if ui.detector != nil {
- config.Enabled = ui.detector.IsEnabled()
- config.QuietForSeconds = ui.detector.GetQuietForSeconds()
- config.FullnessRatio = ui.detector.GetFullnessRatio()
- config.CollectionFilter = ui.detector.GetCollectionFilter()
- config.ScanIntervalSeconds = durationToSeconds(ui.detector.ScanInterval())
- }
-
- // Get current values from scheduler
- if ui.scheduler != nil {
- config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
- }
-
- return config
-}
-
// RegisterUI registers the erasure coding UI provider with the UI registry
func RegisterUI(uiRegistry *types.UIRegistry, detector *EcDetector, scheduler *Scheduler) {
uiProvider := NewUIProvider(detector, scheduler)
diff --git a/weed/worker/tasks/vacuum/ui.go b/weed/worker/tasks/vacuum/ui.go
index 043bb6727..468163c5d 100644
--- a/weed/worker/tasks/vacuum/ui.go
+++ b/weed/worker/tasks/vacuum/ui.go
@@ -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 := `
-
-
-
-
-
-
- Detection Settings
-
-
-
-` + string(form.Build()) + `
-
-
-
-
-
-
-`
-
- 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)
+// GetCurrentConfig returns the current configuration
+func (ui *UIProvider) GetCurrentConfig() interface{} {
+ config := &VacuumConfig{
+ // Default values from schema (matching task_config_schema.go)
+ Enabled: true,
+ GarbageThreshold: 0.3, // 30%
+ ScanIntervalSeconds: 2 * 60 * 60, // 2 hours
+ MaxConcurrent: 2,
+ MinVolumeAgeSeconds: 24 * 60 * 60, // 24 hours
+ MinIntervalSeconds: 7 * 24 * 60 * 60, // 7 days
}
- // 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
- }
+ // Get current values from detector
+ if ui.detector != nil {
+ config.Enabled = ui.detector.IsEnabled()
+ config.GarbageThreshold = ui.detector.GetGarbageThreshold()
+ config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds())
+ config.MinVolumeAgeSeconds = int(ui.detector.GetMinVolumeAge().Seconds())
}
- // 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)
+ // Get current values from scheduler
+ if ui.scheduler != nil {
+ config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
+ config.MinIntervalSeconds = int(ui.scheduler.GetMinInterval().Seconds())
}
- return config, nil
-}
-
-// GetCurrentConfig returns the current configuration
-func (ui *UIProvider) GetCurrentConfig() interface{} {
- return ui.getCurrentVacuumConfig()
+ return config
}
// 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")
+ // 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(secondsToDuration(vacuumConfig.ScanIntervalSeconds))
- ui.detector.SetMinVolumeAge(secondsToDuration(vacuumConfig.MinVolumeAgeSeconds))
+ 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(secondsToDuration(vacuumConfig.MinIntervalSeconds))
+ ui.scheduler.SetMinInterval(time.Duration(vacuumConfig.MinIntervalSeconds) * time.Second)
}
- 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)
+ 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
}
-// getCurrentVacuumConfig gets the current configuration from detector and scheduler
-func (ui *UIProvider) getCurrentVacuumConfig() *VacuumConfig {
- config := &VacuumConfig{
- // Default values (fallback if detectors/schedulers are nil)
- Enabled: true,
- GarbageThreshold: 0.3,
- ScanIntervalSeconds: 30 * 60,
- MinVolumeAgeSeconds: 1 * 60 * 60,
- MaxConcurrent: 2,
- MinIntervalSeconds: 6 * 60 * 60,
- }
-
- // 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())
- }
-
- // Get current values from scheduler
- if ui.scheduler != nil {
- config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
- config.MinIntervalSeconds = durationToSeconds(ui.scheduler.GetMinInterval())
- }
-
- return config
-}
-
// 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 {
diff --git a/weed/worker/types/task_ui.go b/weed/worker/types/task_ui.go
index 7ad5437f6..69840e00f 100644
--- a/weed/worker/types/task_ui.go
+++ b/weed/worker/types/task_ui.go
@@ -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 := "
\n"
-
- // Special handling for checkbox fields
- if field.Type == "checkbox" {
- checked := ""
- if field.Value.(bool) {
- checked = " checked"
- }
- html += "
\n"
- html += " \n"
- html += " \n"
- html += "
\n"
- // Description for checkbox
- if field.Description != "" {
- html += "
" + field.Description + "
\n"
- }
- html += "
\n"
- return html
- }
-
- // Label for non-checkbox fields
- required := ""
- if field.Required {
- required = " *"
- }
- html += " \n"
-
- // Input based on type
- switch field.Type {
- case "text":
- html += " \n"
-
- case "number":
- html += " \n"
-
- case "select":
- html += " \n"
-
- case "duration":
- html += " \n"
-
- case "interval":
- // Create input group with number input and unit select
- html += "
\n"
- html += " \n"
- html += " \n"
- html += "
\n"
- }
-
- // Description for non-checkbox fields
- if field.Description != "" {
- html += "