diff options
Diffstat (limited to 'weed/worker/tasks/balance/ui_templ.go')
| -rw-r--r-- | weed/worker/tasks/balance/ui_templ.go | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/weed/worker/tasks/balance/ui_templ.go b/weed/worker/tasks/balance/ui_templ.go new file mode 100644 index 000000000..54998af4c --- /dev/null +++ b/weed/worker/tasks/balance/ui_templ.go @@ -0,0 +1,369 @@ +package balance + +import ( + "fmt" + "strconv" + "time" + + "github.com/seaweedfs/seaweedfs/weed/admin/view/components" + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/worker/types" +) + +// Helper function to format seconds as duration string +func formatDurationFromSeconds(seconds int) string { + d := time.Duration(seconds) * time.Second + return d.String() +} + +// Helper functions to convert between seconds and value+unit format +func secondsToValueAndUnit(seconds int) (float64, string) { + if seconds == 0 { + return 0, "minutes" + } + + // Try days first + if seconds%(24*3600) == 0 && seconds >= 24*3600 { + return float64(seconds / (24 * 3600)), "days" + } + + // Try hours + if seconds%3600 == 0 && seconds >= 3600 { + return float64(seconds / 3600), "hours" + } + + // Default to minutes + return float64(seconds / 60), "minutes" +} + +func valueAndUnitToSeconds(value float64, unit string) int { + switch unit { + case "days": + return int(value * 24 * 3600) + case "hours": + return int(value * 3600) + case "minutes": + return int(value * 60) + default: + return int(value * 60) // Default to minutes + } +} + +// UITemplProvider provides the templ-based UI for balance task configuration +type UITemplProvider struct { + detector *BalanceDetector + scheduler *BalanceScheduler +} + +// NewUITemplProvider creates a new balance templ UI provider +func NewUITemplProvider(detector *BalanceDetector, scheduler *BalanceScheduler) *UITemplProvider { + return &UITemplProvider{ + detector: detector, + scheduler: scheduler, + } +} + +// GetTaskType returns the task type +func (ui *UITemplProvider) GetTaskType() types.TaskType { + return types.TaskTypeBalance +} + +// GetDisplayName returns the human-readable name +func (ui *UITemplProvider) GetDisplayName() string { + return "Volume Balance" +} + +// GetDescription returns a description of what this task does +func (ui *UITemplProvider) GetDescription() string { + return "Redistributes volumes across volume servers to optimize storage utilization and performance" +} + +// GetIcon returns the icon CSS class for this task type +func (ui *UITemplProvider) GetIcon() string { + return "fas fa-balance-scale text-secondary" +} + +// RenderConfigSections renders the configuration as templ section data +func (ui *UITemplProvider) RenderConfigSections(currentConfig interface{}) ([]components.ConfigSectionData, error) { + config := ui.getCurrentBalanceConfig() + + // Detection settings section + detectionSection := components.ConfigSectionData{ + Title: "Detection Settings", + Icon: "fas fa-search", + Description: "Configure when balance tasks should be triggered", + Fields: []interface{}{ + components.CheckboxFieldData{ + FormFieldData: components.FormFieldData{ + Name: "enabled", + Label: "Enable Balance Tasks", + Description: "Whether balance tasks should be automatically created", + }, + Checked: config.Enabled, + }, + components.NumberFieldData{ + FormFieldData: components.FormFieldData{ + Name: "imbalance_threshold", + Label: "Imbalance Threshold", + Description: "Trigger balance when storage imbalance exceeds this percentage (0.0-1.0)", + Required: true, + }, + Value: config.ImbalanceThreshold, + Step: "0.01", + Min: floatPtr(0.0), + Max: floatPtr(1.0), + }, + components.DurationInputFieldData{ + FormFieldData: components.FormFieldData{ + Name: "scan_interval", + Label: "Scan Interval", + Description: "How often to scan for imbalanced volumes", + Required: true, + }, + Seconds: config.ScanIntervalSeconds, + }, + }, + } + + // Scheduling settings section + schedulingSection := components.ConfigSectionData{ + Title: "Scheduling Settings", + Icon: "fas fa-clock", + Description: "Configure task scheduling and concurrency", + Fields: []interface{}{ + components.NumberFieldData{ + FormFieldData: components.FormFieldData{ + Name: "max_concurrent", + Label: "Max Concurrent Tasks", + Description: "Maximum number of balance tasks that can run simultaneously", + Required: true, + }, + Value: float64(config.MaxConcurrent), + Step: "1", + Min: floatPtr(1), + }, + components.NumberFieldData{ + FormFieldData: components.FormFieldData{ + Name: "min_server_count", + Label: "Minimum Server Count", + Description: "Only balance when at least this many servers are available", + Required: true, + }, + Value: float64(config.MinServerCount), + Step: "1", + Min: floatPtr(1), + }, + }, + } + + // Timing constraints section + timingSection := components.ConfigSectionData{ + Title: "Timing Constraints", + Icon: "fas fa-calendar-clock", + Description: "Configure when balance operations are allowed", + Fields: []interface{}{ + components.CheckboxFieldData{ + FormFieldData: components.FormFieldData{ + Name: "move_during_off_hours", + Label: "Restrict to Off-Hours", + Description: "Only perform balance operations during off-peak hours", + }, + Checked: config.MoveDuringOffHours, + }, + components.TextFieldData{ + FormFieldData: components.FormFieldData{ + Name: "off_hours_start", + Label: "Off-Hours Start Time", + Description: "Start time for off-hours window (e.g., 23:00)", + }, + Value: config.OffHoursStart, + }, + components.TextFieldData{ + FormFieldData: components.FormFieldData{ + Name: "off_hours_end", + Label: "Off-Hours End Time", + Description: "End time for off-hours window (e.g., 06:00)", + }, + Value: config.OffHoursEnd, + }, + }, + } + + // Performance impact info section + performanceSection := components.ConfigSectionData{ + Title: "Performance Considerations", + Icon: "fas fa-exclamation-triangle", + Description: "Important information about balance operations", + Fields: []interface{}{ + components.TextFieldData{ + FormFieldData: components.FormFieldData{ + Name: "performance_info", + Label: "Performance Impact", + Description: "Volume balancing involves data movement and can impact cluster performance", + }, + Value: "Enable off-hours restriction to minimize impact on production workloads", + }, + components.TextFieldData{ + FormFieldData: components.FormFieldData{ + Name: "safety_info", + Label: "Safety Requirements", + Description: fmt.Sprintf("Requires at least %d servers to ensure data safety during moves", config.MinServerCount), + }, + Value: "Maintains data safety during volume moves between servers", + }, + }, + } + + return []components.ConfigSectionData{detectionSection, schedulingSection, timingSection, performanceSection}, nil +} + +// ParseConfigForm parses form data into configuration +func (ui *UITemplProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) { + config := &BalanceConfig{} + + // Parse enabled checkbox + config.Enabled = len(formData["enabled"]) > 0 && formData["enabled"][0] == "on" + + // Parse imbalance threshold + if thresholdStr := formData["imbalance_threshold"]; len(thresholdStr) > 0 { + if threshold, err := strconv.ParseFloat(thresholdStr[0], 64); err != nil { + return nil, fmt.Errorf("invalid imbalance threshold: %v", err) + } else if threshold < 0 || threshold > 1 { + return nil, fmt.Errorf("imbalance threshold must be between 0.0 and 1.0") + } else { + config.ImbalanceThreshold = threshold + } + } + + // Parse scan interval + if valueStr := formData["scan_interval"]; len(valueStr) > 0 { + if value, err := strconv.ParseFloat(valueStr[0], 64); err != nil { + return nil, fmt.Errorf("invalid scan interval value: %v", err) + } else { + unit := "minutes" // default + if unitStr := formData["scan_interval_unit"]; len(unitStr) > 0 { + unit = unitStr[0] + } + config.ScanIntervalSeconds = valueAndUnitToSeconds(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: %v", err) + } else if concurrent < 1 { + return nil, fmt.Errorf("max concurrent must be at least 1") + } else { + config.MaxConcurrent = concurrent + } + } + + // Parse min server count + if serverCountStr := formData["min_server_count"]; len(serverCountStr) > 0 { + if serverCount, err := strconv.Atoi(serverCountStr[0]); err != nil { + return nil, fmt.Errorf("invalid min server count: %v", err) + } else if serverCount < 1 { + return nil, fmt.Errorf("min server count must be at least 1") + } else { + config.MinServerCount = serverCount + } + } + + // Parse move during off hours + config.MoveDuringOffHours = len(formData["move_during_off_hours"]) > 0 && formData["move_during_off_hours"][0] == "on" + + // Parse off hours start time + if startStr := formData["off_hours_start"]; len(startStr) > 0 { + config.OffHoursStart = startStr[0] + } + + // Parse off hours end time + if endStr := formData["off_hours_end"]; len(endStr) > 0 { + config.OffHoursEnd = endStr[0] + } + + return config, nil +} + +// GetCurrentConfig returns the current configuration +func (ui *UITemplProvider) GetCurrentConfig() interface{} { + return ui.getCurrentBalanceConfig() +} + +// ApplyConfig applies the new configuration +func (ui *UITemplProvider) 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(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) + 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 *UITemplProvider) getCurrentBalanceConfig() *BalanceConfig { + config := &BalanceConfig{ + // Default values (fallback if detectors/schedulers are nil) + Enabled: true, + ImbalanceThreshold: 0.1, // 10% imbalance + ScanIntervalSeconds: int((4 * time.Hour).Seconds()), + MaxConcurrent: 1, + MinServerCount: 3, + MoveDuringOffHours: true, + OffHoursStart: "23:00", + OffHoursEnd: "06:00", + } + + // 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 +} + +// floatPtr is a helper function to create float64 pointers +func floatPtr(f float64) *float64 { + return &f +} + +// RegisterUITempl registers the balance templ UI provider with the UI registry +func RegisterUITempl(uiRegistry *types.UITemplRegistry, detector *BalanceDetector, scheduler *BalanceScheduler) { + uiProvider := NewUITemplProvider(detector, scheduler) + uiRegistry.RegisterUI(uiProvider) + + glog.V(1).Infof("✅ Registered balance task templ UI provider") +} |
