From 319aa61dab1bfd34d2ec9c02e32e18a07f1286bf Mon Sep 17 00:00:00 2001 From: chrislu Date: Sat, 26 Jul 2025 21:23:19 -0700 Subject: [PATCH] MaintenanceConfig use ConfigField --- weed/admin/dash/admin_server.go | 129 +-- weed/admin/handlers/maintenance_handlers.go | 15 +- weed/admin/maintenance/config_schema.go | 488 +++++++++++ weed/admin/maintenance/maintenance_types.go | 36 +- .../view/app/maintenance_config_schema.templ | 389 +++++++++ .../app/maintenance_config_schema_templ.go | 772 ++++++++++++++++++ weed/admin/view/components/form_fields.templ | 69 ++ .../view/components/form_fields_templ.go | 210 +++++ 8 files changed, 2042 insertions(+), 66 deletions(-) create mode 100644 weed/admin/maintenance/config_schema.go create mode 100644 weed/admin/view/app/maintenance_config_schema.templ create mode 100644 weed/admin/view/app/maintenance_config_schema_templ.go diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 5c762762a..1f42680a7 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -1135,7 +1135,7 @@ func (as *AdminServer) getMaintenanceConfig() (*maintenance.MaintenanceConfigDat } // Ensure policy is properly initialized and fix zero values with meaningful defaults - defaultConfig := DefaultMaintenanceConfig() + defaultConfig := maintenance.DefaultMaintenanceConfig() configUpdated := false if config.Policy == nil { @@ -1144,34 +1144,42 @@ func (as *AdminServer) getMaintenanceConfig() (*maintenance.MaintenanceConfigDat glog.V(1).Infof("Initialized null policy with defaults") } - // Replace zero values with meaningful defaults - if config.ScanIntervalSeconds == 0 { - config.ScanIntervalSeconds = defaultConfig.ScanIntervalSeconds - configUpdated = true - } - if config.WorkerTimeoutSeconds == 0 { - config.WorkerTimeoutSeconds = defaultConfig.WorkerTimeoutSeconds - configUpdated = true - } - if config.TaskTimeoutSeconds == 0 { - config.TaskTimeoutSeconds = defaultConfig.TaskTimeoutSeconds - configUpdated = true - } - if config.RetryDelaySeconds == 0 { - config.RetryDelaySeconds = defaultConfig.RetryDelaySeconds - configUpdated = true - } - if config.MaxRetries == 0 { - config.MaxRetries = defaultConfig.MaxRetries - configUpdated = true - } - if config.CleanupIntervalSeconds == 0 { - config.CleanupIntervalSeconds = defaultConfig.CleanupIntervalSeconds - configUpdated = true - } - if config.TaskRetentionSeconds == 0 { - config.TaskRetentionSeconds = defaultConfig.TaskRetentionSeconds + // Apply schema defaults for any zero values + schema := maintenance.GetMaintenanceConfigSchema() + if err := schema.ApplyDefaults(config); err == nil { configUpdated = true + glog.V(1).Infof("Applied schema defaults to configuration") + } else { + glog.V(1).Infof("Schema defaults failed, using fallback: %v", err) + // Fallback to using the default config for zero values + if config.ScanIntervalSeconds == 0 { + config.ScanIntervalSeconds = defaultConfig.ScanIntervalSeconds + configUpdated = true + } + if config.WorkerTimeoutSeconds == 0 { + config.WorkerTimeoutSeconds = defaultConfig.WorkerTimeoutSeconds + configUpdated = true + } + if config.TaskTimeoutSeconds == 0 { + config.TaskTimeoutSeconds = defaultConfig.TaskTimeoutSeconds + configUpdated = true + } + if config.RetryDelaySeconds == 0 { + config.RetryDelaySeconds = defaultConfig.RetryDelaySeconds + configUpdated = true + } + if config.MaxRetries == 0 { + config.MaxRetries = defaultConfig.MaxRetries + configUpdated = true + } + if config.CleanupIntervalSeconds == 0 { + config.CleanupIntervalSeconds = defaultConfig.CleanupIntervalSeconds + configUpdated = true + } + if config.TaskRetentionSeconds == 0 { + config.TaskRetentionSeconds = defaultConfig.TaskRetentionSeconds + configUpdated = true + } } // Save the corrected configuration if any updates were made @@ -1219,34 +1227,47 @@ func (as *AdminServer) getMaintenanceConfig() (*maintenance.MaintenanceConfigDat // updateMaintenanceConfig updates maintenance configuration func (as *AdminServer) updateMaintenanceConfig(config *maintenance.MaintenanceConfig) error { - // Apply meaningful defaults for zero values before saving - defaultConfig := DefaultMaintenanceConfig() - - if config.Policy == nil { - config.Policy = defaultConfig.Policy + // Apply meaningful defaults for zero values before saving using schema + schema := maintenance.GetMaintenanceConfigSchema() + + // Apply schema defaults + if err := schema.ApplyDefaults(config); err != nil { + glog.Errorf("Failed to apply schema defaults: %v", err) + // Fallback to manual default application + defaultConfig := maintenance.DefaultMaintenanceConfig() + if config.Policy == nil { + config.Policy = defaultConfig.Policy + } + if config.ScanIntervalSeconds == 0 { + config.ScanIntervalSeconds = defaultConfig.ScanIntervalSeconds + } + if config.WorkerTimeoutSeconds == 0 { + config.WorkerTimeoutSeconds = defaultConfig.WorkerTimeoutSeconds + } + if config.TaskTimeoutSeconds == 0 { + config.TaskTimeoutSeconds = defaultConfig.TaskTimeoutSeconds + } + if config.RetryDelaySeconds == 0 { + config.RetryDelaySeconds = defaultConfig.RetryDelaySeconds + } + if config.MaxRetries == 0 { + config.MaxRetries = defaultConfig.MaxRetries + } + if config.CleanupIntervalSeconds == 0 { + config.CleanupIntervalSeconds = defaultConfig.CleanupIntervalSeconds + } + if config.TaskRetentionSeconds == 0 { + config.TaskRetentionSeconds = defaultConfig.TaskRetentionSeconds + } } - // Replace zero values with meaningful defaults - if config.ScanIntervalSeconds == 0 { - config.ScanIntervalSeconds = defaultConfig.ScanIntervalSeconds - } - if config.WorkerTimeoutSeconds == 0 { - config.WorkerTimeoutSeconds = defaultConfig.WorkerTimeoutSeconds - } - if config.TaskTimeoutSeconds == 0 { - config.TaskTimeoutSeconds = defaultConfig.TaskTimeoutSeconds - } - if config.RetryDelaySeconds == 0 { - config.RetryDelaySeconds = defaultConfig.RetryDelaySeconds - } - if config.MaxRetries == 0 { - config.MaxRetries = defaultConfig.MaxRetries - } - if config.CleanupIntervalSeconds == 0 { - config.CleanupIntervalSeconds = defaultConfig.CleanupIntervalSeconds - } - if config.TaskRetentionSeconds == 0 { - config.TaskRetentionSeconds = defaultConfig.TaskRetentionSeconds + // Validate configuration using schema + if validationErrors := schema.ValidateConfig(config); len(validationErrors) > 0 { + var errorMessages []string + for _, err := range validationErrors { + errorMessages = append(errorMessages, err.Error()) + } + return fmt.Errorf("configuration validation failed: %v", errorMessages) } // Save configuration to persistent storage diff --git a/weed/admin/handlers/maintenance_handlers.go b/weed/admin/handlers/maintenance_handlers.go index ba5e58ad7..4f14651b4 100644 --- a/weed/admin/handlers/maintenance_handlers.go +++ b/weed/admin/handlers/maintenance_handlers.go @@ -78,21 +78,32 @@ func (h *MaintenanceHandlers) ShowMaintenanceWorkers(c *gin.Context) { // ShowMaintenanceConfig displays the maintenance configuration page func (h *MaintenanceHandlers) ShowMaintenanceConfig(c *gin.Context) { + // DEBUG: Log that this method is being called + glog.Infof("DEBUG: MaintenanceHandlers.ShowMaintenanceConfig called") + config, err := h.getMaintenanceConfig() if err != nil { + glog.Errorf("DEBUG: Error getting maintenance config: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - // Render HTML template + // Get the schema for dynamic form rendering + schema := maintenance.GetMaintenanceConfigSchema() + glog.Infof("DEBUG: Got schema with %d fields", len(schema.Fields)) + + // Render HTML template using schema-driven approach c.Header("Content-Type", "text/html") - configComponent := app.MaintenanceConfig(config) + configComponent := app.MaintenanceConfigSchema(config, schema) layoutComponent := layout.Layout(c, configComponent) + glog.Infof("DEBUG: About to render MaintenanceConfigSchema template") err = layoutComponent.Render(c.Request.Context(), c.Writer) if err != nil { + glog.Errorf("DEBUG: Template render error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) return } + glog.Infof("DEBUG: MaintenanceConfigSchema template rendered successfully") } // ShowTaskConfig displays the configuration page for a specific task type diff --git a/weed/admin/maintenance/config_schema.go b/weed/admin/maintenance/config_schema.go new file mode 100644 index 000000000..d6bfe36cb --- /dev/null +++ b/weed/admin/maintenance/config_schema.go @@ -0,0 +1,488 @@ +package maintenance + +import ( + "fmt" + "reflect" + "strings" + "time" +) + +// ConfigFieldType defines the type of a configuration field +type ConfigFieldType string + +const ( + FieldTypeBool ConfigFieldType = "bool" + FieldTypeInt ConfigFieldType = "int" + FieldTypeDuration ConfigFieldType = "duration" + FieldTypeInterval ConfigFieldType = "interval" + FieldTypeString ConfigFieldType = "string" + FieldTypeFloat ConfigFieldType = "float" +) + +// ConfigFieldUnit defines the unit for display purposes +type ConfigFieldUnit string + +const ( + UnitSeconds ConfigFieldUnit = "seconds" + UnitMinutes ConfigFieldUnit = "minutes" + UnitHours ConfigFieldUnit = "hours" + UnitDays ConfigFieldUnit = "days" + UnitCount ConfigFieldUnit = "count" + UnitNone ConfigFieldUnit = "" +) + +// ConfigField defines a configuration field with all its metadata +type ConfigField struct { + // Field identification + Name string `json:"name"` + JSONName string `json:"json_name"` + Type ConfigFieldType `json:"type"` + + // Default value and validation + DefaultValue interface{} `json:"default_value"` + MinValue interface{} `json:"min_value,omitempty"` + MaxValue interface{} `json:"max_value,omitempty"` + Required bool `json:"required"` + + // UI display + DisplayName string `json:"display_name"` + Description string `json:"description"` + HelpText string `json:"help_text"` + Placeholder string `json:"placeholder"` + Unit ConfigFieldUnit `json:"unit"` + + // Form rendering + InputType string `json:"input_type"` // "checkbox", "number", "text", etc. + CSSClasses string `json:"css_classes,omitempty"` +} + +// GetDisplayValue returns the value formatted for display in the specified unit +func (cf *ConfigField) GetDisplayValue(value interface{}) interface{} { + if (cf.Type == FieldTypeDuration || cf.Type == FieldTypeInterval) && cf.Unit != UnitSeconds { + if duration, ok := value.(time.Duration); ok { + switch cf.Unit { + case UnitMinutes: + return int(duration.Minutes()) + case UnitHours: + return int(duration.Hours()) + case UnitDays: + return int(duration.Hours() / 24) + } + } + if seconds, ok := value.(int); ok { + switch cf.Unit { + case UnitMinutes: + return seconds / 60 + case UnitHours: + return seconds / 3600 + case UnitDays: + return seconds / (24 * 3600) + } + } + } + return value +} + +// GetIntervalDisplayValue returns the value and unit for interval fields +func (cf *ConfigField) GetIntervalDisplayValue(value interface{}) (int, string) { + if cf.Type != FieldTypeInterval { + return 0, "minutes" + } + + seconds := 0 + if duration, ok := value.(time.Duration); ok { + seconds = int(duration.Seconds()) + } else if s, ok := value.(int); ok { + seconds = s + } + + return SecondsToIntervalValueUnit(seconds) +} + +// SecondsToIntervalValueUnit converts seconds to the most appropriate interval unit +func SecondsToIntervalValueUnit(totalSeconds int) (int, string) { + if totalSeconds == 0 { + return 0, "minutes" + } + + // Check if it's evenly divisible by days + if totalSeconds%(24*3600) == 0 { + return totalSeconds / (24 * 3600), "days" + } + + // Check if it's evenly divisible by hours + if totalSeconds%3600 == 0 { + return totalSeconds / 3600, "hours" + } + + // Default to minutes + return totalSeconds / 60, "minutes" +} + +// IntervalValueUnitToSeconds converts interval value and unit to seconds +func IntervalValueUnitToSeconds(value int, unit string) int { + switch unit { + case "days": + return value * 24 * 3600 + case "hours": + return value * 3600 + case "minutes": + return value * 60 + default: + return value * 60 // Default to minutes + } +} + +// ParseDisplayValue converts a display value back to the storage format +func (cf *ConfigField) ParseDisplayValue(displayValue interface{}) interface{} { + if (cf.Type == FieldTypeDuration || cf.Type == FieldTypeInterval) && cf.Unit != UnitSeconds { + if val, ok := displayValue.(int); ok { + switch cf.Unit { + case UnitMinutes: + return val * 60 + case UnitHours: + return val * 3600 + case UnitDays: + return val * 24 * 3600 + } + } + } + return displayValue +} + +// ParseIntervalFormData parses form data for interval fields (value + unit) +func (cf *ConfigField) ParseIntervalFormData(valueStr, unitStr string) (int, error) { + if cf.Type != FieldTypeInterval { + return 0, fmt.Errorf("field %s is not an interval field", cf.Name) + } + + value := 0 + if valueStr != "" { + var err error + value, err = fmt.Sscanf(valueStr, "%d", &value) + if err != nil { + return 0, fmt.Errorf("invalid interval value: %s", valueStr) + } + } + + return IntervalValueUnitToSeconds(value, unitStr), nil +} + +// ValidateValue validates a value against the field constraints +func (cf *ConfigField) ValidateValue(value interface{}) error { + if cf.Required && (value == nil || value == "" || value == 0) { + return fmt.Errorf("%s is required", cf.DisplayName) + } + + if cf.MinValue != nil { + if !cf.compareValues(value, cf.MinValue, ">=") { + return fmt.Errorf("%s must be >= %v", cf.DisplayName, cf.MinValue) + } + } + + if cf.MaxValue != nil { + if !cf.compareValues(value, cf.MaxValue, "<=") { + return fmt.Errorf("%s must be <= %v", cf.DisplayName, cf.MaxValue) + } + } + + return nil +} + +// compareValues compares two values based on the operator +func (cf *ConfigField) compareValues(a, b interface{}, op string) bool { + switch cf.Type { + case FieldTypeInt: + aVal, aOk := a.(int) + bVal, bOk := b.(int) + if !aOk || !bOk { + return false + } + switch op { + case ">=": + return aVal >= bVal + case "<=": + return aVal <= bVal + } + case FieldTypeFloat: + aVal, aOk := a.(float64) + bVal, bOk := b.(float64) + if !aOk || !bOk { + return false + } + switch op { + case ">=": + return aVal >= bVal + case "<=": + return aVal <= bVal + } + } + return true +} + +// MaintenanceConfigSchema defines the schema for maintenance configuration +type MaintenanceConfigSchema struct { + Fields map[string]*ConfigField `json:"fields"` +} + +// GetMaintenanceConfigSchema returns the schema for maintenance configuration +func GetMaintenanceConfigSchema() *MaintenanceConfigSchema { + return &MaintenanceConfigSchema{ + Fields: map[string]*ConfigField{ + "enabled": { + Name: "enabled", + JSONName: "enabled", + Type: FieldTypeBool, + DefaultValue: false, + Required: false, + DisplayName: "Enable Maintenance System", + Description: "When enabled, the system will automatically scan for and execute maintenance tasks", + HelpText: "Toggle this to enable or disable the entire maintenance system", + InputType: "checkbox", + CSSClasses: "form-check-input", + }, + "scan_interval_seconds": { + Name: "scan_interval_seconds", + JSONName: "scan_interval_seconds", + Type: FieldTypeInterval, + DefaultValue: 30 * 60, // 30 minutes in seconds + MinValue: 1 * 60, // 1 minute + MaxValue: 24 * 60 * 60, // 24 hours + Required: true, + DisplayName: "Scan Interval", + Description: "How often to scan for maintenance tasks", + HelpText: "The system will check for new maintenance tasks at this interval", + Placeholder: "30", + Unit: UnitMinutes, + InputType: "interval", + CSSClasses: "form-control", + }, + "worker_timeout_seconds": { + Name: "worker_timeout_seconds", + JSONName: "worker_timeout_seconds", + Type: FieldTypeInterval, + DefaultValue: 5 * 60, // 5 minutes + MinValue: 1 * 60, // 1 minute + MaxValue: 60 * 60, // 1 hour + Required: true, + DisplayName: "Worker Timeout", + Description: "How long to wait for worker heartbeat before considering it inactive", + HelpText: "Workers that don't send heartbeats within this time are considered offline", + Placeholder: "5", + Unit: UnitMinutes, + InputType: "interval", + CSSClasses: "form-control", + }, + "task_timeout_seconds": { + Name: "task_timeout_seconds", + JSONName: "task_timeout_seconds", + Type: FieldTypeInterval, + DefaultValue: 2 * 60 * 60, // 2 hours + MinValue: 1 * 60 * 60, // 1 hour + MaxValue: 24 * 60 * 60, // 24 hours + Required: true, + DisplayName: "Task Timeout", + Description: "Maximum time allowed for a single task to complete", + HelpText: "Tasks that run longer than this will be considered failed and may be retried", + Placeholder: "2", + Unit: UnitHours, + InputType: "interval", + CSSClasses: "form-control", + }, + "retry_delay_seconds": { + Name: "retry_delay_seconds", + JSONName: "retry_delay_seconds", + Type: FieldTypeInterval, + DefaultValue: 15 * 60, // 15 minutes + MinValue: 1 * 60, // 1 minute + MaxValue: 2 * 60 * 60, // 2 hours + Required: true, + DisplayName: "Retry Delay", + Description: "Time to wait before retrying failed tasks", + HelpText: "Failed tasks will wait this long before being retried", + Placeholder: "15", + Unit: UnitMinutes, + InputType: "interval", + CSSClasses: "form-control", + }, + "max_retries": { + Name: "max_retries", + JSONName: "max_retries", + Type: FieldTypeInt, + DefaultValue: 3, + MinValue: 0, + MaxValue: 10, + Required: false, + DisplayName: "Default Max Retries", + Description: "Default number of times to retry failed tasks", + HelpText: "Tasks will be retried this many times before being marked as permanently failed", + Placeholder: "3 (default)", + Unit: UnitCount, + InputType: "number", + CSSClasses: "form-control", + }, + "cleanup_interval_seconds": { + Name: "cleanup_interval_seconds", + JSONName: "cleanup_interval_seconds", + Type: FieldTypeInterval, + DefaultValue: 24 * 60 * 60, // 24 hours + MinValue: 1 * 60 * 60, // 1 hour + MaxValue: 7 * 24 * 60 * 60, // 7 days + Required: true, + DisplayName: "Cleanup Interval", + Description: "How often to clean up old task records", + HelpText: "The system will remove old completed/failed tasks at this interval", + Placeholder: "24", + Unit: UnitHours, + InputType: "interval", + CSSClasses: "form-control", + }, + "task_retention_seconds": { + Name: "task_retention_seconds", + JSONName: "task_retention_seconds", + Type: FieldTypeInterval, + DefaultValue: 7 * 24 * 60 * 60, // 7 days + MinValue: 1 * 24 * 60 * 60, // 1 day + MaxValue: 30 * 24 * 60 * 60, // 30 days + Required: true, + DisplayName: "Task Retention", + Description: "How long to keep completed/failed task records", + HelpText: "Task records older than this will be automatically deleted", + Placeholder: "7", + Unit: UnitDays, + InputType: "interval", + CSSClasses: "form-control", + }, + "global_max_concurrent": { + Name: "global_max_concurrent", + JSONName: "global_max_concurrent", + Type: FieldTypeInt, + DefaultValue: 4, + MinValue: 1, + MaxValue: 20, + Required: true, + DisplayName: "Global Concurrent Limit", + Description: "Maximum number of maintenance tasks that can run simultaneously across all workers", + HelpText: "This limits the total system load from maintenance operations", + Placeholder: "4 (default)", + Unit: UnitCount, + InputType: "number", + CSSClasses: "form-control", + }, + }, + } +} + +// ApplyDefaults applies default values to a configuration struct using reflection +func (schema *MaintenanceConfigSchema) ApplyDefaults(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, exists := schema.Fields[jsonTag] + if !exists { + continue + } + + // Apply default if field is zero value + if field.CanSet() && isZeroValue(field) { + defaultValue := reflect.ValueOf(schemaField.DefaultValue) + if defaultValue.Type().ConvertibleTo(field.Type()) { + field.Set(defaultValue.Convert(field.Type())) + } + } + } + + return nil +} + +// isZeroValue checks if a reflect.Value represents a zero value +func isZeroValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.String: + return v.String() == "" + case reflect.Slice, reflect.Map, reflect.Array: + return v.IsNil() || v.Len() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + } + return false +} + +// ValidateConfig validates a configuration against the schema +func (schema *MaintenanceConfigSchema) ValidateConfig(config interface{}) []error { + var errors []error + + configValue := reflect.ValueOf(config) + if configValue.Kind() == reflect.Ptr { + configValue = configValue.Elem() + } + + if configValue.Kind() != reflect.Struct { + errors = append(errors, fmt.Errorf("config must be a struct or pointer to struct")) + return errors + } + + 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, exists := schema.Fields[jsonTag] + if !exists { + continue + } + + // Validate field value + fieldValue := field.Interface() + if err := schemaField.ValidateValue(fieldValue); err != nil { + errors = append(errors, err) + } + } + + return errors +} diff --git a/weed/admin/maintenance/maintenance_types.go b/weed/admin/maintenance/maintenance_types.go index 8aa4e88b6..aa33033fb 100644 --- a/weed/admin/maintenance/maintenance_types.go +++ b/weed/admin/maintenance/maintenance_types.go @@ -283,21 +283,37 @@ type MaintenanceConfig struct { // Default configuration values func DefaultMaintenanceConfig() *MaintenanceConfig { - return &MaintenanceConfig{ - Enabled: false, // Disabled by default for safety - ScanIntervalSeconds: 30 * 60, // 30 minutes - WorkerTimeoutSeconds: 5 * 60, // 5 minutes - TaskTimeoutSeconds: 2 * 60 * 60, // 2 hours - RetryDelaySeconds: 15 * 60, // 15 minutes - MaxRetries: 3, - CleanupIntervalSeconds: 24 * 60 * 60, // 24 hours - TaskRetentionSeconds: 7 * 24 * 60 * 60, // 7 days + config := &MaintenanceConfig{ Policy: &MaintenancePolicy{ - GlobalMaxConcurrent: 4, DefaultRepeatInterval: 6, DefaultCheckInterval: 12, }, } + + // Apply defaults from schema + schema := GetMaintenanceConfigSchema() + if err := schema.ApplyDefaults(config); err != nil { + // Fallback to hardcoded defaults if schema fails + config.Enabled = false + config.ScanIntervalSeconds = 30 * 60 // 30 minutes + config.WorkerTimeoutSeconds = 5 * 60 // 5 minutes + config.TaskTimeoutSeconds = 2 * 60 * 60 // 2 hours + config.RetryDelaySeconds = 15 * 60 // 15 minutes + config.MaxRetries = 3 + config.CleanupIntervalSeconds = 24 * 60 * 60 // 24 hours + config.TaskRetentionSeconds = 7 * 24 * 60 * 60 // 7 days + } + + // Apply policy defaults from schema + if config.Policy != nil { + if globalMaxField, exists := schema.Fields["global_max_concurrent"]; exists { + if defaultVal, ok := globalMaxField.DefaultValue.(int); ok { + config.Policy.GlobalMaxConcurrent = defaultVal + } + } + } + + return config } // MaintenanceQueueData represents data for the queue visualization UI diff --git a/weed/admin/view/app/maintenance_config_schema.templ b/weed/admin/view/app/maintenance_config_schema.templ new file mode 100644 index 000000000..f58a509b3 --- /dev/null +++ b/weed/admin/view/app/maintenance_config_schema.templ @@ -0,0 +1,389 @@ +package app + +import ( + "fmt" + "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" +) + +templ MaintenanceConfigSchema(data *maintenance.MaintenanceConfigData, schema *maintenance.MaintenanceConfigSchema) { +
+
+
+
+

+ + Maintenance Configuration (SCHEMA_TEMPLATE) +

+ +
+
+
+ +
+
+
+
+
System Settings
+
+
+
+ + @ConfigField(schema.Fields["enabled"], data.Config.Enabled) + @ConfigField(schema.Fields["scan_interval_seconds"], data.Config.ScanIntervalSeconds) + @ConfigField(schema.Fields["worker_timeout_seconds"], data.Config.WorkerTimeoutSeconds) + @ConfigField(schema.Fields["task_timeout_seconds"], data.Config.TaskTimeoutSeconds) + @ConfigField(schema.Fields["retry_delay_seconds"], data.Config.RetryDelaySeconds) + @ConfigField(schema.Fields["max_retries"], data.Config.MaxRetries) + @ConfigField(schema.Fields["cleanup_interval_seconds"], data.Config.CleanupIntervalSeconds) + @ConfigField(schema.Fields["task_retention_seconds"], data.Config.TaskRetentionSeconds) + if data.Config.Policy != nil { + @ConfigField(schema.Fields["global_max_concurrent"], data.Config.Policy.GlobalMaxConcurrent) + } + +
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+ + Task Configuration +
+
+
+

Configure specific settings for each maintenance task type.

+ +
+
+
+
+ + +
+
+
+
+
System Statistics
+
+
+
+
+
+
Last Scan
+

{data.LastScanTime.Format("2006-01-02 15:04:05")}

+
+
+
+
+
Next Scan
+

{data.NextScanTime.Format("2006-01-02 15:04:05")}

+
+
+
+
+
Total Tasks
+

{fmt.Sprintf("%d", data.SystemStats.TotalTasks)}

+
+
+
+
+
Active Workers
+

{fmt.Sprintf("%d", data.SystemStats.ActiveWorkers)}

+
+
+
+
+
+
+
+
+ + +} + +// ConfigField renders a single configuration field based on schema +templ ConfigField(field *maintenance.ConfigField, value interface{}) { + if field.InputType == "interval" { + +
+ +
+ + +
+ if field.Description != "" { +
{ field.Description }
+ } +
+ } else if field.InputType == "checkbox" { + +
+
+ + +
+ if field.Description != "" { +
{ field.Description }
+ } +
+ } else { + +
+ + + if field.Description != "" { +
{ field.Description }
+ } +
+ } +} + +// Helper functions for the template +func getBoolValue(value interface{}) bool { + if boolVal, ok := value.(bool); ok { + return boolVal + } + return false +} + +func convertSecondsToDisplayValue(value interface{}, field *maintenance.ConfigField) float64 { + if intVal, ok := value.(int); ok { + if intVal == 0 { + return 0 + } + + // Check if it's evenly divisible by days + if intVal%(24*3600) == 0 { + return float64(intVal / (24 * 3600)) + } + + // Check if it's evenly divisible by hours + if intVal%3600 == 0 { + return float64(intVal / 3600) + } + + // Default to minutes + return float64(intVal / 60) + } + return 0 +} + +func getDisplayUnit(value interface{}, field *maintenance.ConfigField) string { + if intVal, ok := value.(int); ok { + if intVal == 0 { + return "minutes" + } + + // Check if it's evenly divisible by days + if intVal%(24*3600) == 0 { + return "days" + } + + // Check if it's evenly divisible by hours + if intVal%3600 == 0 { + return "hours" + } + + // Default to minutes + return "minutes" + } + return "minutes" +} + +// Helper function to convert schema to JSON for JavaScript +templ schemaToJSON(schema *maintenance.MaintenanceConfigSchema) { + {`{}`} +} \ No newline at end of file diff --git a/weed/admin/view/app/maintenance_config_schema_templ.go b/weed/admin/view/app/maintenance_config_schema_templ.go new file mode 100644 index 000000000..2685ef0e5 --- /dev/null +++ b/weed/admin/view/app/maintenance_config_schema_templ.go @@ -0,0 +1,772 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.906 +package app + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" +) + +func MaintenanceConfigSchema(data *maintenance.MaintenanceConfigData, schema *maintenance.MaintenanceConfigSchema) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Maintenance Configuration (SCHEMA_TEMPLATE)

System Settings
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ConfigField(schema.Fields["enabled"], data.Config.Enabled).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ConfigField(schema.Fields["scan_interval_seconds"], data.Config.ScanIntervalSeconds).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ConfigField(schema.Fields["worker_timeout_seconds"], data.Config.WorkerTimeoutSeconds).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ConfigField(schema.Fields["task_timeout_seconds"], data.Config.TaskTimeoutSeconds).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ConfigField(schema.Fields["retry_delay_seconds"], data.Config.RetryDelaySeconds).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ConfigField(schema.Fields["max_retries"], data.Config.MaxRetries).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ConfigField(schema.Fields["cleanup_interval_seconds"], data.Config.CleanupIntervalSeconds).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ConfigField(schema.Fields["task_retention_seconds"], data.Config.TaskRetentionSeconds).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Config.Policy != nil { + templ_7745c5c3_Err = ConfigField(schema.Fields["global_max_concurrent"], data.Config.Policy.GlobalMaxConcurrent).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Task Configuration

Configure specific settings for each maintenance task type.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, menuItem := range data.MenuItems { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 = []any{menuItem.Icon + " me-2"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(menuItem.DisplayName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 82, Col: 65} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if menuItem.IsEnabled { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Enabled") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Disabled") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(menuItem.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 90, Col: 90} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
System Statistics
Last Scan

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastScanTime.Format("2006-01-02 15:04:05")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 111, Col: 100} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

Next Scan

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.NextScanTime.Format("2006-01-02 15:04:05")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 117, Col: 100} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

Total Tasks

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.SystemStats.TotalTasks)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 123, Col: 99} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

Active Workers

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.SystemStats.ActiveWorkers)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 129, Col: 102} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// ConfigField renders a single configuration field based on schema +func ConfigField(field *maintenance.ConfigField, value interface{}) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if field.InputType == "interval" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if field.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(field.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 277, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if field.InputType == "checkbox" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if field.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(field.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 298, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if field.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(field.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 328, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +// Helper functions for the template +func getBoolValue(value interface{}) bool { + if boolVal, ok := value.(bool); ok { + return boolVal + } + return false +} + +func convertSecondsToDisplayValue(value interface{}, field *maintenance.ConfigField) float64 { + if intVal, ok := value.(int); ok { + if intVal == 0 { + return 0 + } + + // Check if it's evenly divisible by days + if intVal%(24*3600) == 0 { + return float64(intVal / (24 * 3600)) + } + + // Check if it's evenly divisible by hours + if intVal%3600 == 0 { + return float64(intVal / 3600) + } + + // Default to minutes + return float64(intVal / 60) + } + return 0 +} + +func getDisplayUnit(value interface{}, field *maintenance.ConfigField) string { + if intVal, ok := value.(int); ok { + if intVal == 0 { + return "minutes" + } + + // Check if it's evenly divisible by days + if intVal%(24*3600) == 0 { + return "days" + } + + // Check if it's evenly divisible by hours + if intVal%3600 == 0 { + return "hours" + } + + // Default to minutes + return "minutes" + } + return "minutes" +} + +// Helper function to convert schema to JSON for JavaScript +func schemaToJSON(schema *maintenance.MaintenanceConfigSchema) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var34 := templ.GetChildren(ctx) + if templ_7745c5c3_Var34 == nil { + templ_7745c5c3_Var34 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(`{}`) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_config_schema.templ`, Line: 388, Col: 9} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/weed/admin/view/components/form_fields.templ b/weed/admin/view/components/form_fields.templ index 5ac5c9241..18b20f9fe 100644 --- a/weed/admin/view/components/form_fields.templ +++ b/weed/admin/view/components/form_fields.templ @@ -303,4 +303,73 @@ func convertSecondsToValue(seconds int, unit string) float64 { default: return float64(seconds / 60) // Default to minutes } +} + +// IntervalFieldData represents interval input field data with separate value and unit +type IntervalFieldData struct { + FormFieldData + Seconds int // The interval value in seconds +} + +// IntervalField renders a Bootstrap interval input with number + unit dropdown (like task config) +templ IntervalField(data IntervalFieldData) { +
+ +
+ + +
+ if data.Description != "" { +
{ data.Description }
+ } +
} \ No newline at end of file diff --git a/weed/admin/view/components/form_fields_templ.go b/weed/admin/view/components/form_fields_templ.go index 937082a17..2997d5fdd 100644 --- a/weed/admin/view/components/form_fields_templ.go +++ b/weed/admin/view/components/form_fields_templ.go @@ -1101,4 +1101,214 @@ func convertSecondsToValue(seconds int, unit string) float64 { } } +// IntervalFieldData represents interval input field data with separate value and unit +type IntervalFieldData struct { + FormFieldData + Seconds int // The interval value in seconds +} + +// IntervalField renders a Bootstrap interval input with number + unit dropdown (like task config) +func IntervalField(data IntervalFieldData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var50 := templ.GetChildren(ctx) + if templ_7745c5c3_Var50 == nil { + templ_7745c5c3_Var50 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var58 string + templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(data.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/components/form_fields.templ`, Line: 372, Col: 55} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + var _ = templruntime.GeneratedTemplate