Browse Source

reduce duplication

worker-execute-ec-tasks
chrislu 4 months ago
parent
commit
f7140d4577
  1. 199
      weed/admin/handlers/maintenance_handlers.go
  2. 313
      weed/worker/tasks/balance/ui.go
  3. 270
      weed/worker/tasks/erasure_coding/ui.go
  4. 279
      weed/worker/tasks/vacuum/ui.go
  5. 245
      weed/worker/types/task_ui.go

199
weed/admin/handlers/maintenance_handlers.go

@ -1,16 +1,24 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"reflect"
"strconv"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "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/dash"
"github.com/seaweedfs/seaweedfs/weed/admin/maintenance" "github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
"github.com/seaweedfs/seaweedfs/weed/admin/view/app" "github.com/seaweedfs/seaweedfs/weed/admin/view/app"
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout" "github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
"github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/worker/tasks" "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" "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) { func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) {
taskTypeName := c.Param("taskType") 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 // Parse form data
err := c.Request.ParseForm() err := c.Request.ParseForm()
@ -182,31 +181,51 @@ func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) {
return 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{} 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() uiRegistry := tasks.GetGlobalUIRegistry()
typesRegistry := tasks.GetGlobalTypesRegistry() typesRegistry := tasks.GetGlobalTypesRegistry()
@ -223,25 +242,123 @@ func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) {
return 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) err = provider.ApplyConfig(config)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to apply configuration: " + err.Error()})
return return
} }
// } // End of disabled templ UI provider else block
// Redirect back to task configuration page // Redirect back to task configuration page
c.Redirect(http.StatusSeeOther, "/maintenance/config/"+taskTypeName) 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 // UpdateMaintenanceConfig updates maintenance configuration from form
func (h *MaintenanceHandlers) UpdateMaintenanceConfig(c *gin.Context) { func (h *MaintenanceHandlers) UpdateMaintenanceConfig(c *gin.Context) {
var config maintenance.MaintenanceConfig var config maintenance.MaintenanceConfig

313
weed/worker/tasks/balance/ui.go

@ -1,12 +1,10 @@
package balance package balance
import ( import (
"fmt"
"html/template"
"strconv"
"time" "time"
"github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
"github.com/seaweedfs/seaweedfs/weed/worker/types" "github.com/seaweedfs/seaweedfs/weed/worker/types"
) )
@ -44,253 +42,69 @@ func (ui *UIProvider) GetIcon() string {
return "fas fa-balance-scale text-secondary" return "fas fa-balance-scale text-secondary"
} }
// BalanceConfig represents the balance configuration
// BalanceConfig represents the balance configuration matching the schema
type BalanceConfig struct { type BalanceConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ImbalanceThreshold float64 `json:"imbalance_threshold"` ImbalanceThreshold float64 `json:"imbalance_threshold"`
ScanIntervalSeconds int `json:"scan_interval_seconds"` ScanIntervalSeconds int `json:"scan_interval_seconds"`
MaxConcurrent int `json:"max_concurrent"` MaxConcurrent int `json:"max_concurrent"`
MinServerCount int `json:"min_server_count"` 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]
// 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,
} }
// 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 detector
if ui.detector != nil {
config.Enabled = ui.detector.IsEnabled()
config.ImbalanceThreshold = ui.detector.GetThreshold()
config.ScanIntervalSeconds = int(ui.detector.ScanInterval().Seconds())
} }
return config, nil
// Get current values from scheduler
if ui.scheduler != nil {
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
config.MinServerCount = ui.scheduler.GetMinServerCount()
} }
// GetCurrentConfig returns the current configuration
func (ui *UIProvider) GetCurrentConfig() interface{} {
return ui.getCurrentBalanceConfig()
return config
} }
// ApplyConfig applies the new configuration // ApplyConfig applies the new configuration
func (ui *UIProvider) ApplyConfig(config interface{}) error { func (ui *UIProvider) ApplyConfig(config interface{}) error {
balanceConfig, ok := config.(*BalanceConfig) balanceConfig, ok := config.(*BalanceConfig)
if !ok { 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 // Apply to detector
if ui.detector != nil { if ui.detector != nil {
ui.detector.SetEnabled(balanceConfig.Enabled) ui.detector.SetEnabled(balanceConfig.Enabled)
ui.detector.SetThreshold(balanceConfig.ImbalanceThreshold) ui.detector.SetThreshold(balanceConfig.ImbalanceThreshold)
ui.detector.SetMinCheckInterval(secondsToDuration(balanceConfig.ScanIntervalSeconds))
ui.detector.SetMinCheckInterval(time.Duration(balanceConfig.ScanIntervalSeconds) * time.Second)
} }
// Apply to scheduler // Apply to scheduler
@ -298,52 +112,15 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
ui.scheduler.SetEnabled(balanceConfig.Enabled) ui.scheduler.SetEnabled(balanceConfig.Enabled)
ui.scheduler.SetMaxConcurrent(balanceConfig.MaxConcurrent) ui.scheduler.SetMaxConcurrent(balanceConfig.MaxConcurrent)
ui.scheduler.SetMinServerCount(balanceConfig.MinServerCount) 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.Enabled, balanceConfig.ImbalanceThreshold*100, balanceConfig.MaxConcurrent,
balanceConfig.MinServerCount, balanceConfig.MoveDuringOffHours)
balanceConfig.MinServerCount)
return nil 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 // RegisterUI registers the balance UI provider with the UI registry
func RegisterUI(uiRegistry *types.UIRegistry, detector *BalanceDetector, scheduler *BalanceScheduler) { func RegisterUI(uiRegistry *types.UIRegistry, detector *BalanceDetector, scheduler *BalanceScheduler) {
uiProvider := NewUIProvider(detector, scheduler) uiProvider := NewUIProvider(detector, scheduler)
@ -356,13 +133,9 @@ func RegisterUI(uiRegistry *types.UIRegistry, detector *BalanceDetector, schedul
func DefaultBalanceConfig() *BalanceConfig { func DefaultBalanceConfig() *BalanceConfig {
return &BalanceConfig{ return &BalanceConfig{
Enabled: false, 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, MinServerCount: 3,
MoveDuringOffHours: false,
OffHoursStart: "22:00",
OffHoursEnd: "06:00",
MinIntervalSeconds: durationToSeconds(1 * time.Hour),
} }
} }

270
weed/worker/tasks/erasure_coding/ui.go

@ -1,12 +1,10 @@
package erasure_coding package erasure_coding
import ( import (
"fmt"
"html/template"
"strconv"
"time" "time"
"github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
"github.com/seaweedfs/seaweedfs/weed/worker/types" "github.com/seaweedfs/seaweedfs/weed/worker/types"
) )
@ -44,220 +42,65 @@ func (ui *UIProvider) GetIcon() string {
return "fas fa-shield-alt text-info" 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 { type ErasureCodingConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
QuietForSeconds int `json:"quiet_for_seconds"` QuietForSeconds int `json:"quiet_for_seconds"`
FullnessRatio float64 `json:"fullness_ratio"`
ScanIntervalSeconds int `json:"scan_interval_seconds"` ScanIntervalSeconds int `json:"scan_interval_seconds"`
MaxConcurrent int `json:"max_concurrent"` MaxConcurrent int `json:"max_concurrent"`
FullnessRatio float64 `json:"fullness_ratio"`
CollectionFilter string `json:"collection_filter"` 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
// 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 collection filter
if values, ok := formData["collection_filter"]; ok && len(values) > 0 {
config.CollectionFilter = values[0]
// 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())
} }
return config, nil
// Get current values from scheduler
if ui.scheduler != nil {
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
} }
// GetCurrentConfig returns the current configuration
func (ui *UIProvider) GetCurrentConfig() interface{} {
return ui.getCurrentECConfig()
return config
} }
// ApplyConfig applies the new configuration // ApplyConfig applies the new configuration
func (ui *UIProvider) ApplyConfig(config interface{}) error { func (ui *UIProvider) ApplyConfig(config interface{}) error {
ecConfig, ok := config.(ErasureCodingConfig) ecConfig, ok := config.(ErasureCodingConfig)
if !ok { 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 // Apply to detector
@ -266,7 +109,7 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
ui.detector.SetQuietForSeconds(ecConfig.QuietForSeconds) ui.detector.SetQuietForSeconds(ecConfig.QuietForSeconds)
ui.detector.SetFullnessRatio(ecConfig.FullnessRatio) ui.detector.SetFullnessRatio(ecConfig.FullnessRatio)
ui.detector.SetCollectionFilter(ecConfig.CollectionFilter) ui.detector.SetCollectionFilter(ecConfig.CollectionFilter)
ui.detector.SetScanInterval(secondsToDuration(ecConfig.ScanIntervalSeconds))
ui.detector.SetScanInterval(time.Duration(ecConfig.ScanIntervalSeconds) * time.Second)
} }
// Apply to scheduler // Apply to scheduler
@ -275,41 +118,12 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
ui.scheduler.SetMaxConcurrent(ecConfig.MaxConcurrent) 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) ecConfig.Enabled, ecConfig.QuietForSeconds, ecConfig.MaxConcurrent, ecConfig.FullnessRatio, ecConfig.CollectionFilter)
return nil 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 // RegisterUI registers the erasure coding UI provider with the UI registry
func RegisterUI(uiRegistry *types.UIRegistry, detector *EcDetector, scheduler *Scheduler) { func RegisterUI(uiRegistry *types.UIRegistry, detector *EcDetector, scheduler *Scheduler) {
uiProvider := NewUIProvider(detector, scheduler) uiProvider := NewUIProvider(detector, scheduler)

279
weed/worker/tasks/vacuum/ui.go

@ -1,12 +1,10 @@
package vacuum package vacuum
import ( import (
"fmt"
"html/template"
"strconv"
"time" "time"
"github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
"github.com/seaweedfs/seaweedfs/weed/worker/types" "github.com/seaweedfs/seaweedfs/weed/worker/types"
) )
@ -44,7 +42,7 @@ func (ui *UIProvider) GetIcon() string {
return "fas fa-broom text-primary" return "fas fa-broom text-primary"
} }
// VacuumConfig represents the vacuum configuration
// VacuumConfig represents the vacuum configuration matching the schema
type VacuumConfig struct { type VacuumConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
GarbageThreshold float64 `json:"garbage_threshold"` GarbageThreshold float64 `json:"garbage_threshold"`
@ -54,267 +52,78 @@ type VacuumConfig struct {
MinIntervalSeconds int `json:"min_interval_seconds"` 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]
// 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
} }
// Convert to seconds using the helper function from types package
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
// 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 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)
// Get current values from scheduler
if ui.scheduler != nil {
config.MaxConcurrent = ui.scheduler.GetMaxConcurrent()
config.MinIntervalSeconds = int(ui.scheduler.GetMinInterval().Seconds())
} }
unit := "minute" // default
if units, ok := formData["min_volume_age_seconds_unit"]; ok && len(units) > 0 {
unit = units[0]
return config
} }
// Convert to seconds
config.MinVolumeAgeSeconds = types.IntervalValueUnitToSeconds(value, unit)
// 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
} }
// 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")
// Use reflection to convert to VacuumConfig - simplified approach
if vc, ok := config.(*VacuumConfig); ok {
vacuumConfig = vc
} else { } else {
config.MaxConcurrent = concurrent
glog.Warningf("Config type conversion failed, using current config")
vacuumConfig = ui.GetCurrentConfig().(*VacuumConfig)
} }
} }
// 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 // Apply to detector
if ui.detector != nil { if ui.detector != nil {
ui.detector.SetEnabled(vacuumConfig.Enabled) ui.detector.SetEnabled(vacuumConfig.Enabled)
ui.detector.SetGarbageThreshold(vacuumConfig.GarbageThreshold) 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 // Apply to scheduler
if ui.scheduler != nil { if ui.scheduler != nil {
ui.scheduler.SetEnabled(vacuumConfig.Enabled) ui.scheduler.SetEnabled(vacuumConfig.Enabled)
ui.scheduler.SetMaxConcurrent(vacuumConfig.MaxConcurrent) 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 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 // RegisterUI registers the vacuum UI provider with the UI registry
func RegisterUI(uiRegistry *types.UIRegistry, detector *VacuumDetector, scheduler *VacuumScheduler) { func RegisterUI(uiRegistry *types.UIRegistry, detector *VacuumDetector, scheduler *VacuumScheduler) {
uiProvider := NewUIProvider(detector, scheduler) 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") 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 { func GetUIProvider(uiRegistry *types.UIRegistry) *UIProvider {
provider := uiRegistry.GetProvider(types.TaskTypeVacuum) provider := uiRegistry.GetProvider(types.TaskTypeVacuum)
if provider == nil { if provider == nil {

245
weed/worker/types/task_ui.go

@ -1,8 +1,6 @@
package types package types
import ( import (
"fmt"
"html/template"
"time" "time"
) )
@ -41,6 +39,7 @@ func IntervalValueUnitToSeconds(value int, unit string) int {
} }
// TaskUIProvider defines how tasks provide their configuration UI // TaskUIProvider defines how tasks provide their configuration UI
// This interface is simplified to work with schema-driven configuration
type TaskUIProvider interface { type TaskUIProvider interface {
// GetTaskType returns the task type // GetTaskType returns the task type
GetTaskType() TaskType GetTaskType() TaskType
@ -54,12 +53,6 @@ type TaskUIProvider interface {
// GetIcon returns the icon CSS class or HTML for this task type // GetIcon returns the icon CSS class or HTML for this task type
GetIcon() string 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 returns the current configuration
GetCurrentConfig() interface{} GetCurrentConfig() interface{}
@ -126,241 +119,5 @@ type TaskDetailsData struct {
DisplayName string `json:"display_name"` DisplayName string `json:"display_name"`
Description string `json:"description"` Description string `json:"description"`
Stats *TaskStats `json:"stats"` Stats *TaskStats `json:"stats"`
ConfigForm template.HTML `json:"config_form"`
LastUpdated time.Time `json:"last_updated"` 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
}
Loading…
Cancel
Save