9 changed files with 2503 additions and 931 deletions
-
350weed/admin/config/schema.go
-
39weed/admin/handlers/maintenance_handlers.go
-
630weed/admin/maintenance/config_schema.go
-
2weed/admin/maintenance/maintenance_types.go
-
274weed/admin/view/app/maintenance_config_schema.templ
-
554weed/admin/view/app/maintenance_config_schema_templ.go
-
396weed/admin/view/app/task_config_schema.templ
-
877weed/admin/view/app/task_config_schema_templ.go
-
310weed/worker/tasks/task_config_schema.go
@ -0,0 +1,350 @@ |
|||||
|
package config |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"reflect" |
||||
|
"strings" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
// FieldType defines the type of a configuration field
|
||||
|
type FieldType string |
||||
|
|
||||
|
const ( |
||||
|
FieldTypeBool FieldType = "bool" |
||||
|
FieldTypeInt FieldType = "int" |
||||
|
FieldTypeDuration FieldType = "duration" |
||||
|
FieldTypeInterval FieldType = "interval" |
||||
|
FieldTypeString FieldType = "string" |
||||
|
FieldTypeFloat FieldType = "float" |
||||
|
) |
||||
|
|
||||
|
// FieldUnit defines the unit for display purposes
|
||||
|
type FieldUnit string |
||||
|
|
||||
|
const ( |
||||
|
UnitSeconds FieldUnit = "seconds" |
||||
|
UnitMinutes FieldUnit = "minutes" |
||||
|
UnitHours FieldUnit = "hours" |
||||
|
UnitDays FieldUnit = "days" |
||||
|
UnitCount FieldUnit = "count" |
||||
|
UnitNone FieldUnit = "" |
||||
|
) |
||||
|
|
||||
|
// Field defines a configuration field with all its metadata
|
||||
|
type Field struct { |
||||
|
// Field identification
|
||||
|
Name string `json:"name"` |
||||
|
JSONName string `json:"json_name"` |
||||
|
Type FieldType `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 FieldUnit `json:"unit"` |
||||
|
|
||||
|
// Form rendering
|
||||
|
InputType string `json:"input_type"` // "checkbox", "number", "text", "interval", etc.
|
||||
|
CSSClasses string `json:"css_classes,omitempty"` |
||||
|
} |
||||
|
|
||||
|
// GetDisplayValue returns the value formatted for display in the specified unit
|
||||
|
func (f *Field) GetDisplayValue(value interface{}) interface{} { |
||||
|
if (f.Type == FieldTypeDuration || f.Type == FieldTypeInterval) && f.Unit != UnitSeconds { |
||||
|
if duration, ok := value.(time.Duration); ok { |
||||
|
switch f.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 f.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 (f *Field) GetIntervalDisplayValue(value interface{}) (int, string) { |
||||
|
if f.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 (f *Field) ParseDisplayValue(displayValue interface{}) interface{} { |
||||
|
if (f.Type == FieldTypeDuration || f.Type == FieldTypeInterval) && f.Unit != UnitSeconds { |
||||
|
if val, ok := displayValue.(int); ok { |
||||
|
switch f.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 (f *Field) ParseIntervalFormData(valueStr, unitStr string) (int, error) { |
||||
|
if f.Type != FieldTypeInterval { |
||||
|
return 0, fmt.Errorf("field %s is not an interval field", f.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 (f *Field) ValidateValue(value interface{}) error { |
||||
|
if f.Required && (value == nil || value == "" || value == 0) { |
||||
|
return fmt.Errorf("%s is required", f.DisplayName) |
||||
|
} |
||||
|
|
||||
|
if f.MinValue != nil { |
||||
|
if !f.compareValues(value, f.MinValue, ">=") { |
||||
|
return fmt.Errorf("%s must be >= %v", f.DisplayName, f.MinValue) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if f.MaxValue != nil { |
||||
|
if !f.compareValues(value, f.MaxValue, "<=") { |
||||
|
return fmt.Errorf("%s must be <= %v", f.DisplayName, f.MaxValue) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// compareValues compares two values based on the operator
|
||||
|
func (f *Field) compareValues(a, b interface{}, op string) bool { |
||||
|
switch f.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 |
||||
|
} |
||||
|
|
||||
|
// Schema provides common functionality for configuration schemas
|
||||
|
type Schema struct { |
||||
|
Fields []*Field `json:"fields"` |
||||
|
} |
||||
|
|
||||
|
// GetFieldByName returns a field by its JSON name
|
||||
|
func (s *Schema) GetFieldByName(jsonName string) *Field { |
||||
|
for _, field := range s.Fields { |
||||
|
if field.JSONName == jsonName { |
||||
|
return field |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// ApplyDefaults applies default values to a configuration struct using reflection
|
||||
|
func (s *Schema) 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 := s.GetFieldByName(jsonTag) |
||||
|
if schemaField == nil { |
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// ValidateConfig validates a configuration against the schema
|
||||
|
func (s *Schema) 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 := s.GetFieldByName(jsonTag) |
||||
|
if schemaField == nil { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// Validate field value
|
||||
|
fieldValue := field.Interface() |
||||
|
if err := schemaField.ValidateValue(fieldValue); err != nil { |
||||
|
errors = append(errors, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return errors |
||||
|
} |
||||
|
|
||||
|
// 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 |
||||
|
} |
||||
@ -1,488 +1,190 @@ |
|||||
package maintenance |
package maintenance |
||||
|
|
||||
import ( |
import ( |
||||
"fmt" |
|
||||
"reflect" |
|
||||
"strings" |
|
||||
"time" |
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/admin/config" |
||||
) |
) |
||||
|
|
||||
// ConfigFieldType defines the type of a configuration field
|
|
||||
type ConfigFieldType string |
|
||||
|
// Type aliases for backward compatibility
|
||||
|
type ConfigFieldType = config.FieldType |
||||
|
type ConfigFieldUnit = config.FieldUnit |
||||
|
type ConfigField = config.Field |
||||
|
|
||||
|
// Constant aliases for backward compatibility
|
||||
const ( |
const ( |
||||
FieldTypeBool ConfigFieldType = "bool" |
|
||||
FieldTypeInt ConfigFieldType = "int" |
|
||||
FieldTypeDuration ConfigFieldType = "duration" |
|
||||
FieldTypeInterval ConfigFieldType = "interval" |
|
||||
FieldTypeString ConfigFieldType = "string" |
|
||||
FieldTypeFloat ConfigFieldType = "float" |
|
||||
|
FieldTypeBool = config.FieldTypeBool |
||||
|
FieldTypeInt = config.FieldTypeInt |
||||
|
FieldTypeDuration = config.FieldTypeDuration |
||||
|
FieldTypeInterval = config.FieldTypeInterval |
||||
|
FieldTypeString = config.FieldTypeString |
||||
|
FieldTypeFloat = config.FieldTypeFloat |
||||
) |
) |
||||
|
|
||||
// ConfigFieldUnit defines the unit for display purposes
|
|
||||
type ConfigFieldUnit string |
|
||||
|
|
||||
const ( |
const ( |
||||
UnitSeconds ConfigFieldUnit = "seconds" |
|
||||
UnitMinutes ConfigFieldUnit = "minutes" |
|
||||
UnitHours ConfigFieldUnit = "hours" |
|
||||
UnitDays ConfigFieldUnit = "days" |
|
||||
UnitCount ConfigFieldUnit = "count" |
|
||||
UnitNone ConfigFieldUnit = "" |
|
||||
|
UnitSeconds = config.UnitSeconds |
||||
|
UnitMinutes = config.UnitMinutes |
||||
|
UnitHours = config.UnitHours |
||||
|
UnitDays = config.UnitDays |
||||
|
UnitCount = config.UnitCount |
||||
|
UnitNone = config.UnitNone |
||||
) |
) |
||||
|
|
||||
// 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 |
|
||||
} |
|
||||
|
// Function aliases for backward compatibility
|
||||
|
var ( |
||||
|
SecondsToIntervalValueUnit = config.SecondsToIntervalValueUnit |
||||
|
IntervalValueUnitToSeconds = config.IntervalValueUnitToSeconds |
||||
|
) |
||||
|
|
||||
// MaintenanceConfigSchema defines the schema for maintenance configuration
|
// MaintenanceConfigSchema defines the schema for maintenance configuration
|
||||
type MaintenanceConfigSchema struct { |
type MaintenanceConfigSchema struct { |
||||
Fields map[string]*ConfigField `json:"fields"` |
|
||||
|
config.Schema // Embed common schema functionality
|
||||
} |
} |
||||
|
|
||||
// GetMaintenanceConfigSchema returns the schema for maintenance configuration
|
// GetMaintenanceConfigSchema returns the schema for maintenance configuration
|
||||
func GetMaintenanceConfigSchema() *MaintenanceConfigSchema { |
func GetMaintenanceConfigSchema() *MaintenanceConfigSchema { |
||||
return &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", |
|
||||
|
Schema: config.Schema{ |
||||
|
Fields: []*config.Field{ |
||||
|
{ |
||||
|
Name: "enabled", |
||||
|
JSONName: "enabled", |
||||
|
Type: config.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", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "scan_interval_seconds", |
||||
|
JSONName: "scan_interval_seconds", |
||||
|
Type: config.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: config.UnitMinutes, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "worker_timeout_seconds", |
||||
|
JSONName: "worker_timeout_seconds", |
||||
|
Type: config.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: config.UnitMinutes, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "task_timeout_seconds", |
||||
|
JSONName: "task_timeout_seconds", |
||||
|
Type: config.FieldTypeInterval, |
||||
|
DefaultValue: 2 * 60 * 60, // 2 hours
|
||||
|
MinValue: 10 * 60, // 10 minutes
|
||||
|
MaxValue: 24 * 60 * 60, // 24 hours
|
||||
|
Required: true, |
||||
|
DisplayName: "Task Timeout", |
||||
|
Description: "Maximum time allowed for a task to complete", |
||||
|
HelpText: "Tasks that exceed this duration will be marked as failed", |
||||
|
Placeholder: "2", |
||||
|
Unit: config.UnitHours, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "retry_delay_seconds", |
||||
|
JSONName: "retry_delay_seconds", |
||||
|
Type: config.FieldTypeInterval, |
||||
|
DefaultValue: 15 * 60, // 15 minutes
|
||||
|
MinValue: 1 * 60, // 1 minute
|
||||
|
MaxValue: 24 * 60 * 60, // 24 hours
|
||||
|
Required: true, |
||||
|
DisplayName: "Retry Delay", |
||||
|
Description: "How long to wait before retrying a failed task", |
||||
|
HelpText: "Failed tasks will be retried after this delay", |
||||
|
Placeholder: "15", |
||||
|
Unit: config.UnitMinutes, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "max_retries", |
||||
|
JSONName: "max_retries", |
||||
|
Type: config.FieldTypeInt, |
||||
|
DefaultValue: 3, |
||||
|
MinValue: 0, |
||||
|
MaxValue: 10, |
||||
|
Required: true, |
||||
|
DisplayName: "Max Retries", |
||||
|
Description: "Maximum number of times to retry a failed task", |
||||
|
HelpText: "Tasks that fail more than this many times will be marked as permanently failed", |
||||
|
Placeholder: "3", |
||||
|
Unit: config.UnitCount, |
||||
|
InputType: "number", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "cleanup_interval_seconds", |
||||
|
JSONName: "cleanup_interval_seconds", |
||||
|
Type: config.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 run maintenance cleanup operations", |
||||
|
HelpText: "Removes old task records and temporary files at this interval", |
||||
|
Placeholder: "24", |
||||
|
Unit: config.UnitHours, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "task_retention_seconds", |
||||
|
JSONName: "task_retention_seconds", |
||||
|
Type: config.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 task records", |
||||
|
HelpText: "Task history older than this duration will be automatically deleted", |
||||
|
Placeholder: "7", |
||||
|
Unit: config.UnitDays, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "global_max_concurrent", |
||||
|
JSONName: "global_max_concurrent", |
||||
|
Type: config.FieldTypeInt, |
||||
|
DefaultValue: 10, |
||||
|
MinValue: 1, |
||||
|
MaxValue: 100, |
||||
|
Required: true, |
||||
|
DisplayName: "Global Max Concurrent Tasks", |
||||
|
Description: "Maximum number of maintenance tasks that can run simultaneously across all workers", |
||||
|
HelpText: "Limits the total number of maintenance operations to control system load", |
||||
|
Placeholder: "10", |
||||
|
Unit: config.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 |
|
||||
} |
|
||||
554
weed/admin/view/app/maintenance_config_schema_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,396 @@ |
|||||
|
package app |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"reflect" |
||||
|
"strings" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/admin/maintenance" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/admin/config" |
||||
|
) |
||||
|
|
||||
|
templ TaskConfigSchema(data *maintenance.TaskConfigData, schema *tasks.TaskConfigSchema, config interface{}) { |
||||
|
<div class="container-fluid"> |
||||
|
<div class="row mb-4"> |
||||
|
<div class="col-12"> |
||||
|
<div class="d-flex justify-content-between align-items-center"> |
||||
|
<h2 class="mb-0"> |
||||
|
<i class={schema.Icon + " me-2"}></i> |
||||
|
{schema.DisplayName} Configuration |
||||
|
</h2> |
||||
|
<div class="btn-group"> |
||||
|
<a href="/maintenance/config" class="btn btn-outline-secondary"> |
||||
|
<i class="fas fa-arrow-left me-1"></i> |
||||
|
Back to System Config |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-12"> |
||||
|
<div class="card"> |
||||
|
<div class="card-header"> |
||||
|
<h5 class="mb-0">{schema.DisplayName} Settings</h5> |
||||
|
<p class="mb-0 text-muted">{schema.Description}</p> |
||||
|
</div> |
||||
|
<div class="card-body"> |
||||
|
<form id="taskConfigForm" method="POST"> |
||||
|
<!-- Dynamically render all schema fields in defined order --> |
||||
|
for _, field := range schema.Fields { |
||||
|
@TaskConfigField(field, getTaskFieldValue(config, field.JSONName)) |
||||
|
} |
||||
|
|
||||
|
<div class="d-flex gap-2"> |
||||
|
<button type="submit" class="btn btn-primary"> |
||||
|
<i class="fas fa-save me-1"></i> |
||||
|
Save Configuration |
||||
|
</button> |
||||
|
<button type="button" class="btn btn-secondary" onclick="resetToDefaults()"> |
||||
|
<i class="fas fa-undo me-1"></i> |
||||
|
Reset to Defaults |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Performance Notes Card --> |
||||
|
<div class="row mt-4"> |
||||
|
<div class="col-12"> |
||||
|
<div class="card"> |
||||
|
<div class="card-header"> |
||||
|
<h5 class="mb-0"> |
||||
|
<i class="fas fa-info-circle me-2"></i> |
||||
|
Important Notes |
||||
|
</h5> |
||||
|
</div> |
||||
|
<div class="card-body"> |
||||
|
<div class="alert alert-info" role="alert"> |
||||
|
if schema.TaskName == "vacuum" { |
||||
|
<h6 class="alert-heading">Vacuum Operations:</h6> |
||||
|
<p class="mb-2"><strong>Performance:</strong> Vacuum operations are I/O intensive and may impact cluster performance.</p> |
||||
|
<p class="mb-2"><strong>Safety:</strong> Only volumes meeting age and garbage thresholds will be processed.</p> |
||||
|
<p class="mb-0"><strong>Recommendation:</strong> Monitor cluster load and adjust concurrent limits accordingly.</p> |
||||
|
} else if schema.TaskName == "balance" { |
||||
|
<h6 class="alert-heading">Balance Operations:</h6> |
||||
|
<p class="mb-2"><strong>Performance:</strong> Volume balancing involves data movement and can impact cluster performance.</p> |
||||
|
<p class="mb-2"><strong>Safety:</strong> Requires adequate server count to ensure data safety during moves.</p> |
||||
|
<p class="mb-0"><strong>Recommendation:</strong> Run during off-peak hours to minimize impact on production workloads.</p> |
||||
|
} else if schema.TaskName == "erasure_coding" { |
||||
|
<h6 class="alert-heading">Erasure Coding Operations:</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> Fullness ratio should be between 0.5 and 1.0 (e.g., 0.90 for 90%).</p> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<script> |
||||
|
function resetToDefaults() { |
||||
|
if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) { |
||||
|
// Reset form fields to their default values |
||||
|
const form = document.getElementById('taskConfigForm'); |
||||
|
const schemaFields = window.taskConfigSchema ? window.taskConfigSchema.fields : {}; |
||||
|
|
||||
|
Object.keys(schemaFields).forEach(fieldName => { |
||||
|
const field = schemaFields[fieldName]; |
||||
|
const element = document.getElementById(fieldName); |
||||
|
|
||||
|
if (element && field.default_value !== undefined) { |
||||
|
if (field.input_type === 'checkbox') { |
||||
|
element.checked = field.default_value; |
||||
|
} else if (field.input_type === 'interval') { |
||||
|
// Handle interval fields with value and unit |
||||
|
const valueElement = document.getElementById(fieldName + '_value'); |
||||
|
const unitElement = document.getElementById(fieldName + '_unit'); |
||||
|
if (valueElement && unitElement && field.default_value) { |
||||
|
const defaultSeconds = field.default_value; |
||||
|
const { value, unit } = convertSecondsToTaskIntervalValueUnit(defaultSeconds); |
||||
|
valueElement.value = value; |
||||
|
unitElement.value = unit; |
||||
|
} |
||||
|
} else { |
||||
|
element.value = field.default_value; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function convertSecondsToTaskIntervalValueUnit(totalSeconds) { |
||||
|
if (totalSeconds === 0) { |
||||
|
return { value: 0, unit: 'minutes' }; |
||||
|
} |
||||
|
|
||||
|
// Check if it's evenly divisible by days |
||||
|
if (totalSeconds % (24 * 3600) === 0) { |
||||
|
return { value: totalSeconds / (24 * 3600), unit: 'days' }; |
||||
|
} |
||||
|
|
||||
|
// Check if it's evenly divisible by hours |
||||
|
if (totalSeconds % 3600 === 0) { |
||||
|
return { value: totalSeconds / 3600, unit: 'hours' }; |
||||
|
} |
||||
|
|
||||
|
// Default to minutes |
||||
|
return { value: totalSeconds / 60, unit: 'minutes' }; |
||||
|
} |
||||
|
|
||||
|
// Store schema data for JavaScript access |
||||
|
window.taskConfigSchema = @taskSchemaToTaskJSON(schema); |
||||
|
</script> |
||||
|
} |
||||
|
|
||||
|
// TaskConfigField renders a single task configuration field based on schema |
||||
|
templ TaskConfigField(field *config.Field, value interface{}) { |
||||
|
if field.InputType == "interval" { |
||||
|
<!-- Interval field with number input + unit dropdown --> |
||||
|
<div class="mb-3"> |
||||
|
<label for={ field.JSONName } class="form-label"> |
||||
|
{ field.DisplayName } |
||||
|
if field.Required { |
||||
|
<span class="text-danger">*</span> |
||||
|
} |
||||
|
</label> |
||||
|
<div class="input-group"> |
||||
|
<input |
||||
|
type="number" |
||||
|
class="form-control" |
||||
|
id={ field.JSONName + "_value" } |
||||
|
name={ field.JSONName + "_value" } |
||||
|
value={ fmt.Sprintf("%.0f", convertTaskSecondsToDisplayValue(value, field)) } |
||||
|
step="1" |
||||
|
min="1" |
||||
|
if field.Required { |
||||
|
required |
||||
|
} |
||||
|
/> |
||||
|
<select |
||||
|
class="form-select" |
||||
|
id={ field.JSONName + "_unit" } |
||||
|
name={ field.JSONName + "_unit" } |
||||
|
style="max-width: 120px;" |
||||
|
if field.Required { |
||||
|
required |
||||
|
} |
||||
|
> |
||||
|
<option |
||||
|
value="minutes" |
||||
|
if getTaskDisplayUnit(value, field) == "minutes" { |
||||
|
selected |
||||
|
} |
||||
|
> |
||||
|
Minutes |
||||
|
</option> |
||||
|
<option |
||||
|
value="hours" |
||||
|
if getTaskDisplayUnit(value, field) == "hours" { |
||||
|
selected |
||||
|
} |
||||
|
> |
||||
|
Hours |
||||
|
</option> |
||||
|
<option |
||||
|
value="days" |
||||
|
if getTaskDisplayUnit(value, field) == "days" { |
||||
|
selected |
||||
|
} |
||||
|
> |
||||
|
Days |
||||
|
</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
if field.Description != "" { |
||||
|
<div class="form-text text-muted">{ field.Description }</div> |
||||
|
} |
||||
|
</div> |
||||
|
} else if field.InputType == "checkbox" { |
||||
|
<!-- Checkbox field --> |
||||
|
<div class="mb-3"> |
||||
|
<div class="form-check form-switch"> |
||||
|
<input |
||||
|
class="form-check-input" |
||||
|
type="checkbox" |
||||
|
id={ field.JSONName } |
||||
|
name={ field.JSONName } |
||||
|
if getTaskBoolValue(value) { |
||||
|
checked |
||||
|
} |
||||
|
/> |
||||
|
<label class="form-check-label" for={ field.JSONName }> |
||||
|
<strong>{ field.DisplayName }</strong> |
||||
|
</label> |
||||
|
</div> |
||||
|
if field.Description != "" { |
||||
|
<div class="form-text text-muted">{ field.Description }</div> |
||||
|
} |
||||
|
</div> |
||||
|
} else if field.InputType == "text" { |
||||
|
<!-- Text field --> |
||||
|
<div class="mb-3"> |
||||
|
<label for={ field.JSONName } class="form-label"> |
||||
|
{ field.DisplayName } |
||||
|
if field.Required { |
||||
|
<span class="text-danger">*</span> |
||||
|
} |
||||
|
</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
class="form-control" |
||||
|
id={ field.JSONName } |
||||
|
name={ field.JSONName } |
||||
|
value={ fmt.Sprintf("%v", value) } |
||||
|
placeholder={ field.Placeholder } |
||||
|
if field.Required { |
||||
|
required |
||||
|
} |
||||
|
/> |
||||
|
if field.Description != "" { |
||||
|
<div class="form-text text-muted">{ field.Description }</div> |
||||
|
} |
||||
|
</div> |
||||
|
} else { |
||||
|
<!-- Number field --> |
||||
|
<div class="mb-3"> |
||||
|
<label for={ field.JSONName } class="form-label"> |
||||
|
{ field.DisplayName } |
||||
|
if field.Required { |
||||
|
<span class="text-danger">*</span> |
||||
|
} |
||||
|
</label> |
||||
|
<input |
||||
|
type="number" |
||||
|
class="form-control" |
||||
|
id={ field.JSONName } |
||||
|
name={ field.JSONName } |
||||
|
value={ fmt.Sprintf("%v", value) } |
||||
|
placeholder={ field.Placeholder } |
||||
|
if field.MinValue != nil { |
||||
|
min={ fmt.Sprintf("%v", field.MinValue) } |
||||
|
} |
||||
|
if field.MaxValue != nil { |
||||
|
max={ fmt.Sprintf("%v", field.MaxValue) } |
||||
|
} |
||||
|
step={ getTaskNumberStep(field) } |
||||
|
if field.Required { |
||||
|
required |
||||
|
} |
||||
|
/> |
||||
|
if field.Description != "" { |
||||
|
<div class="form-text text-muted">{ field.Description }</div> |
||||
|
} |
||||
|
</div> |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Helper functions for the template |
||||
|
func getTaskBoolValue(value interface{}) bool { |
||||
|
if boolVal, ok := value.(bool); ok { |
||||
|
return boolVal |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func convertTaskSecondsToDisplayValue(value interface{}, field *config.Field) 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 getTaskDisplayUnit(value interface{}, field *config.Field) 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" |
||||
|
} |
||||
|
|
||||
|
func getTaskNumberStep(field *config.Field) string { |
||||
|
if field.Type == config.FieldTypeFloat { |
||||
|
return "0.01" |
||||
|
} |
||||
|
return "1" |
||||
|
} |
||||
|
|
||||
|
func getTaskFieldValue(config interface{}, fieldName string) interface{} { |
||||
|
if config == nil { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Use reflection to get the field value from the config struct |
||||
|
configValue := reflect.ValueOf(config) |
||||
|
if configValue.Kind() == reflect.Ptr { |
||||
|
configValue = configValue.Elem() |
||||
|
} |
||||
|
|
||||
|
if configValue.Kind() != reflect.Struct { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
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] |
||||
|
} |
||||
|
|
||||
|
if jsonTag == fieldName { |
||||
|
return field.Interface() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Helper function to convert schema to JSON for JavaScript |
||||
|
templ taskSchemaToTaskJSON(schema *tasks.TaskConfigSchema) { |
||||
|
{`{}`} |
||||
|
} |
||||
@ -0,0 +1,877 @@ |
|||||
|
// 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/config" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/admin/maintenance" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/worker/tasks" |
||||
|
"reflect" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
func TaskConfigSchema(data *maintenance.TaskConfigData, schema *tasks.TaskConfigSchema, config 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_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, "<div class=\"container-fluid\"><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"d-flex justify-content-between align-items-center\"><h2 class=\"mb-0\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var2 = []any{schema.Icon + " me-2"} |
||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<i class=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var3 string |
||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String()) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 1, Col: 0} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"></i> ") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var4 string |
||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(schema.DisplayName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 19, Col: 43} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " Configuration</h2><div class=\"btn-group\"><a href=\"/maintenance/config\" class=\"btn btn-outline-secondary\"><i class=\"fas fa-arrow-left me-1\"></i> Back to System Config</a></div></div></div></div><div class=\"row\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"mb-0\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var5 string |
||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(schema.DisplayName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 35, Col: 60} |
||||
|
} |
||||
|
_, 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, 5, " Settings</h5><p class=\"mb-0 text-muted\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var6 string |
||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(schema.Description) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 36, Col: 70} |
||||
|
} |
||||
|
_, 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, 6, "</p></div><div class=\"card-body\"><form id=\"taskConfigForm\" method=\"POST\"><!-- Dynamically render all schema fields in defined order -->") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
for _, field := range schema.Fields { |
||||
|
templ_7745c5c3_Err = TaskConfigField(field, getTaskFieldValue(config, field.JSONName)).Render(ctx, templ_7745c5c3_Buffer) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"d-flex gap-2\"><button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-save me-1\"></i> Save Configuration</button> <button type=\"button\" class=\"btn btn-secondary\" onclick=\"resetToDefaults()\"><i class=\"fas fa-undo me-1\"></i> Reset to Defaults</button></div></form></div></div></div></div><!-- Performance Notes Card --><div class=\"row mt-4\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-header\"><h5 class=\"mb-0\"><i class=\"fas fa-info-circle me-2\"></i> Important Notes</h5></div><div class=\"card-body\"><div class=\"alert alert-info\" role=\"alert\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if schema.TaskName == "vacuum" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<h6 class=\"alert-heading\">Vacuum Operations:</h6><p class=\"mb-2\"><strong>Performance:</strong> Vacuum operations are I/O intensive and may impact cluster performance.</p><p class=\"mb-2\"><strong>Safety:</strong> Only volumes meeting age and garbage thresholds will be processed.</p><p class=\"mb-0\"><strong>Recommendation:</strong> Monitor cluster load and adjust concurrent limits accordingly.</p>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} else if schema.TaskName == "balance" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<h6 class=\"alert-heading\">Balance Operations:</h6><p class=\"mb-2\"><strong>Performance:</strong> Volume balancing involves data movement and can impact cluster performance.</p><p class=\"mb-2\"><strong>Safety:</strong> Requires adequate server count to ensure data safety during moves.</p><p class=\"mb-0\"><strong>Recommendation:</strong> Run during off-peak hours to minimize impact on production workloads.</p>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} else if schema.TaskName == "erasure_coding" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h6 class=\"alert-heading\">Erasure Coding Operations:</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> Fullness ratio should be between 0.5 and 1.0 (e.g., 0.90 for 90%).</p>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div></div></div></div></div><script>\n function resetToDefaults() {\n if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {\n // Reset form fields to their default values\n const form = document.getElementById('taskConfigForm');\n const schemaFields = window.taskConfigSchema ? window.taskConfigSchema.fields : {};\n \n Object.keys(schemaFields).forEach(fieldName => {\n const field = schemaFields[fieldName];\n const element = document.getElementById(fieldName);\n \n if (element && field.default_value !== undefined) {\n if (field.input_type === 'checkbox') {\n element.checked = field.default_value;\n } else if (field.input_type === 'interval') {\n // Handle interval fields with value and unit\n const valueElement = document.getElementById(fieldName + '_value');\n const unitElement = document.getElementById(fieldName + '_unit');\n if (valueElement && unitElement && field.default_value) {\n const defaultSeconds = field.default_value;\n const { value, unit } = convertSecondsToTaskIntervalValueUnit(defaultSeconds);\n valueElement.value = value;\n unitElement.value = unit;\n }\n } else {\n element.value = field.default_value;\n }\n }\n });\n }\n }\n\n function convertSecondsToTaskIntervalValueUnit(totalSeconds) {\n if (totalSeconds === 0) {\n return { value: 0, unit: 'minutes' };\n }\n\n // Check if it's evenly divisible by days\n if (totalSeconds % (24 * 3600) === 0) {\n return { value: totalSeconds / (24 * 3600), unit: 'days' };\n }\n\n // Check if it's evenly divisible by hours\n if (totalSeconds % 3600 === 0) {\n return { value: totalSeconds / 3600, unit: 'hours' };\n }\n\n // Default to minutes\n return { value: totalSeconds / 60, unit: 'minutes' };\n }\n\n // Store schema data for JavaScript access\n window.taskConfigSchema = @taskSchemaToTaskJSON(schema);\n </script>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
return nil |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// TaskConfigField renders a single task configuration field based on schema
|
||||
|
func TaskConfigField(field *config.Field, 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_Var7 := templ.GetChildren(ctx) |
||||
|
if templ_7745c5c3_Var7 == nil { |
||||
|
templ_7745c5c3_Var7 = templ.NopComponent |
||||
|
} |
||||
|
ctx = templ.ClearChildren(ctx) |
||||
|
if field.InputType == "interval" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<!-- Interval field with number input + unit dropdown --> <div class=\"mb-3\"><label for=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var8 string |
||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 157, Col: 39} |
||||
|
} |
||||
|
_, 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, 13, "\" class=\"form-label\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var9 string |
||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(field.DisplayName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 158, Col: 35} |
||||
|
} |
||||
|
_, 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, 14, " ") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Required { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<span class=\"text-danger\">*</span>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</label><div class=\"input-group\"><input type=\"number\" class=\"form-control\" id=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var10 string |
||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName + "_value") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 167, Col: 50} |
||||
|
} |
||||
|
_, 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, 17, "\" name=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var11 string |
||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName + "_value") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 168, Col: 52} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" value=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var12 string |
||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", convertTaskSecondsToDisplayValue(value, field))) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 169, Col: 95} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" step=\"1\" min=\"1\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Required { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " required") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "> <select class=\"form-select\" id=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var13 string |
||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName + "_unit") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 178, Col: 49} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" name=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var14 string |
||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName + "_unit") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 179, Col: 51} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" style=\"max-width: 120px;\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Required { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " required") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "><option value=\"minutes\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if getTaskDisplayUnit(value, field) == "minutes" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " selected") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ">Minutes</option> <option value=\"hours\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if getTaskDisplayUnit(value, field) == "hours" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Hours</option> <option value=\"days\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if getTaskDisplayUnit(value, field) == "days" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " selected") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">Days</option></select></div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Description != "" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div class=\"form-text text-muted\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var15 string |
||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(field.Description) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 212, Col: 69} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} else if field.InputType == "checkbox" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<!-- Checkbox field --> <div class=\"mb-3\"><div class=\"form-check form-switch\"><input class=\"form-check-input\" type=\"checkbox\" id=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var16 string |
||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 222, Col: 39} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" name=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var17 string |
||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 223, Col: 41} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if getTaskBoolValue(value) { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " checked") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "> <label class=\"form-check-label\" for=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var18 string |
||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 228, Col: 68} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\"><strong>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var19 string |
||||
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(field.DisplayName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 229, Col: 47} |
||||
|
} |
||||
|
_, 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, 41, "</strong></label></div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Description != "" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<div class=\"form-text text-muted\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var20 string |
||||
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(field.Description) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 233, Col: 69} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} else if field.InputType == "text" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<!-- Text field --> <div class=\"mb-3\"><label for=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var21 string |
||||
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 239, Col: 39} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" class=\"form-label\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var22 string |
||||
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(field.DisplayName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 240, Col: 35} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " ") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Required { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<span class=\"text-danger\">*</span>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</label> <input type=\"text\" class=\"form-control\" id=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var23 string |
||||
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 248, Col: 35} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\" name=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var24 string |
||||
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 249, Col: 37} |
||||
|
} |
||||
|
_, 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, 51, "\" value=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var25 string |
||||
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", value)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 250, Col: 48} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\" placeholder=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var26 string |
||||
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(field.Placeholder) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 251, Col: 47} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Required { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " required") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "> ") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Description != "" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<div class=\"form-text text-muted\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var27 string |
||||
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(field.Description) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 257, Col: 69} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} else { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<!-- Number field --> <div class=\"mb-3\"><label for=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var28 string |
||||
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 263, Col: 39} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" class=\"form-label\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var29 string |
||||
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(field.DisplayName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 264, Col: 35} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " ") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Required { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<span class=\"text-danger\">*</span>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</label> <input type=\"number\" class=\"form-control\" id=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var30 string |
||||
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 272, Col: 35} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\" name=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var31 string |
||||
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(field.JSONName) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 273, Col: 37} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" value=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var32 string |
||||
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", value)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 274, Col: 48} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" placeholder=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var33 string |
||||
|
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(field.Placeholder) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 275, Col: 47} |
||||
|
} |
||||
|
_, 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, 67, "\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.MinValue != nil { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " min=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var34 string |
||||
|
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", field.MinValue)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 277, Col: 59} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
if field.MaxValue != nil { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " max=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var35 string |
||||
|
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", field.MaxValue)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 280, Col: 59} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " step=\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var36 string |
||||
|
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(getTaskNumberStep(field)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 282, Col: 47} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\"") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Required { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, " required") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "> ") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
if field.Description != "" { |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<div class=\"form-text text-muted\">") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
var templ_7745c5c3_Var37 string |
||||
|
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(field.Description) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 288, Col: 69} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "</div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</div>") |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
} |
||||
|
return nil |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Helper functions for the template
|
||||
|
func getTaskBoolValue(value interface{}) bool { |
||||
|
if boolVal, ok := value.(bool); ok { |
||||
|
return boolVal |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func convertTaskSecondsToDisplayValue(value interface{}, field *config.Field) 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 getTaskDisplayUnit(value interface{}, field *config.Field) 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" |
||||
|
} |
||||
|
|
||||
|
func getTaskNumberStep(field *config.Field) string { |
||||
|
if field.Type == config.FieldTypeFloat { |
||||
|
return "0.01" |
||||
|
} |
||||
|
return "1" |
||||
|
} |
||||
|
|
||||
|
func getTaskFieldValue(config interface{}, fieldName string) interface{} { |
||||
|
if config == nil { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Use reflection to get the field value from the config struct
|
||||
|
configValue := reflect.ValueOf(config) |
||||
|
if configValue.Kind() == reflect.Ptr { |
||||
|
configValue = configValue.Elem() |
||||
|
} |
||||
|
|
||||
|
if configValue.Kind() != reflect.Struct { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
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] |
||||
|
} |
||||
|
|
||||
|
if jsonTag == fieldName { |
||||
|
return field.Interface() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Helper function to convert schema to JSON for JavaScript
|
||||
|
func taskSchemaToTaskJSON(schema *tasks.TaskConfigSchema) 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_Var38 := templ.GetChildren(ctx) |
||||
|
if templ_7745c5c3_Var38 == nil { |
||||
|
templ_7745c5c3_Var38 = templ.NopComponent |
||||
|
} |
||||
|
ctx = templ.ClearChildren(ctx) |
||||
|
var templ_7745c5c3_Var39 string |
||||
|
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(`{}`) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/task_config_schema.templ`, Line: 395, Col: 9} |
||||
|
} |
||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) |
||||
|
if templ_7745c5c3_Err != nil { |
||||
|
return templ_7745c5c3_Err |
||||
|
} |
||||
|
return nil |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
var _ = templruntime.GeneratedTemplate |
||||
@ -0,0 +1,310 @@ |
|||||
|
package tasks |
||||
|
|
||||
|
import ( |
||||
|
"github.com/seaweedfs/seaweedfs/weed/admin/config" |
||||
|
) |
||||
|
|
||||
|
// TaskConfigSchema defines the schema for task configuration
|
||||
|
type TaskConfigSchema struct { |
||||
|
config.Schema // Embed common schema functionality
|
||||
|
TaskName string `json:"task_name"` |
||||
|
DisplayName string `json:"display_name"` |
||||
|
Description string `json:"description"` |
||||
|
Icon string `json:"icon"` |
||||
|
} |
||||
|
|
||||
|
// GetTaskConfigSchema returns the schema for the specified task type
|
||||
|
func GetTaskConfigSchema(taskType string) *TaskConfigSchema { |
||||
|
switch taskType { |
||||
|
case "vacuum": |
||||
|
return GetVacuumTaskConfigSchema() |
||||
|
case "balance": |
||||
|
return GetBalanceTaskConfigSchema() |
||||
|
case "erasure_coding": |
||||
|
return GetErasureCodingTaskConfigSchema() |
||||
|
default: |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// GetVacuumTaskConfigSchema returns the schema for vacuum task configuration
|
||||
|
func GetVacuumTaskConfigSchema() *TaskConfigSchema { |
||||
|
return &TaskConfigSchema{ |
||||
|
TaskName: "vacuum", |
||||
|
DisplayName: "Volume Vacuum", |
||||
|
Description: "Reclaims disk space by removing deleted files from volumes", |
||||
|
Icon: "fas fa-broom text-primary", |
||||
|
Schema: config.Schema{ |
||||
|
Fields: []*config.Field{ |
||||
|
{ |
||||
|
Name: "enabled", |
||||
|
JSONName: "enabled", |
||||
|
Type: config.FieldTypeBool, |
||||
|
DefaultValue: true, |
||||
|
Required: false, |
||||
|
DisplayName: "Enable Vacuum Tasks", |
||||
|
Description: "Whether vacuum tasks should be automatically created", |
||||
|
HelpText: "Toggle this to enable or disable automatic vacuum task generation", |
||||
|
InputType: "checkbox", |
||||
|
CSSClasses: "form-check-input", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "garbage_threshold", |
||||
|
JSONName: "garbage_threshold", |
||||
|
Type: config.FieldTypeFloat, |
||||
|
DefaultValue: 0.3, // 30%
|
||||
|
MinValue: 0.0, |
||||
|
MaxValue: 1.0, |
||||
|
Required: true, |
||||
|
DisplayName: "Garbage Percentage Threshold", |
||||
|
Description: "Trigger vacuum when garbage ratio exceeds this percentage", |
||||
|
HelpText: "Volumes with more deleted content than this threshold will be vacuumed", |
||||
|
Placeholder: "0.30 (30%)", |
||||
|
Unit: config.UnitNone, |
||||
|
InputType: "number", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "scan_interval_seconds", |
||||
|
JSONName: "scan_interval_seconds", |
||||
|
Type: config.FieldTypeInterval, |
||||
|
DefaultValue: 2 * 60 * 60, // 2 hours
|
||||
|
MinValue: 10 * 60, // 10 minutes
|
||||
|
MaxValue: 24 * 60 * 60, // 24 hours
|
||||
|
Required: true, |
||||
|
DisplayName: "Scan Interval", |
||||
|
Description: "How often to scan for volumes needing vacuum", |
||||
|
HelpText: "The system will check for volumes that need vacuuming at this interval", |
||||
|
Placeholder: "2", |
||||
|
Unit: config.UnitHours, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "max_concurrent", |
||||
|
JSONName: "max_concurrent", |
||||
|
Type: config.FieldTypeInt, |
||||
|
DefaultValue: 2, |
||||
|
MinValue: 1, |
||||
|
MaxValue: 10, |
||||
|
Required: true, |
||||
|
DisplayName: "Max Concurrent Tasks", |
||||
|
Description: "Maximum number of vacuum tasks that can run simultaneously", |
||||
|
HelpText: "Limits the number of vacuum operations running at the same time to control system load", |
||||
|
Placeholder: "2 (default)", |
||||
|
Unit: config.UnitCount, |
||||
|
InputType: "number", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "min_volume_age_seconds", |
||||
|
JSONName: "min_volume_age_seconds", |
||||
|
Type: config.FieldTypeInterval, |
||||
|
DefaultValue: 24 * 60 * 60, // 24 hours
|
||||
|
MinValue: 1 * 60 * 60, // 1 hour
|
||||
|
MaxValue: 7 * 24 * 60 * 60, // 7 days
|
||||
|
Required: true, |
||||
|
DisplayName: "Minimum Volume Age", |
||||
|
Description: "Only vacuum volumes older than this duration", |
||||
|
HelpText: "Prevents vacuuming of recently created volumes that may still be actively written to", |
||||
|
Placeholder: "24", |
||||
|
Unit: config.UnitHours, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "min_interval_seconds", |
||||
|
JSONName: "min_interval_seconds", |
||||
|
Type: config.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: "Minimum Interval", |
||||
|
Description: "Minimum time between vacuum operations on the same volume", |
||||
|
HelpText: "Prevents excessive vacuuming of the same volume by enforcing a minimum wait time", |
||||
|
Placeholder: "7", |
||||
|
Unit: config.UnitDays, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// GetBalanceTaskConfigSchema returns the schema for balance task configuration
|
||||
|
func GetBalanceTaskConfigSchema() *TaskConfigSchema { |
||||
|
return &TaskConfigSchema{ |
||||
|
TaskName: "balance", |
||||
|
DisplayName: "Volume Balance", |
||||
|
Description: "Redistributes volumes across volume servers to optimize storage utilization", |
||||
|
Icon: "fas fa-balance-scale text-secondary", |
||||
|
Schema: config.Schema{ |
||||
|
Fields: []*config.Field{ |
||||
|
{ |
||||
|
Name: "enabled", |
||||
|
JSONName: "enabled", |
||||
|
Type: config.FieldTypeBool, |
||||
|
DefaultValue: true, |
||||
|
Required: false, |
||||
|
DisplayName: "Enable Balance Tasks", |
||||
|
Description: "Whether balance tasks should be automatically created", |
||||
|
InputType: "checkbox", |
||||
|
CSSClasses: "form-check-input", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "imbalance_threshold", |
||||
|
JSONName: "imbalance_threshold", |
||||
|
Type: config.FieldTypeFloat, |
||||
|
DefaultValue: 0.1, // 10%
|
||||
|
MinValue: 0.01, |
||||
|
MaxValue: 0.5, |
||||
|
Required: true, |
||||
|
DisplayName: "Imbalance Threshold", |
||||
|
Description: "Trigger balance when storage imbalance exceeds this ratio", |
||||
|
Placeholder: "0.10 (10%)", |
||||
|
Unit: config.UnitNone, |
||||
|
InputType: "number", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "scan_interval_seconds", |
||||
|
JSONName: "scan_interval_seconds", |
||||
|
Type: config.FieldTypeInterval, |
||||
|
DefaultValue: 6 * 60 * 60, // 6 hours
|
||||
|
MinValue: 1 * 60 * 60, // 1 hour
|
||||
|
MaxValue: 24 * 60 * 60, // 24 hours
|
||||
|
Required: true, |
||||
|
DisplayName: "Scan Interval", |
||||
|
Description: "How often to scan for imbalanced volumes", |
||||
|
Unit: config.UnitHours, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "max_concurrent", |
||||
|
JSONName: "max_concurrent", |
||||
|
Type: config.FieldTypeInt, |
||||
|
DefaultValue: 2, |
||||
|
MinValue: 1, |
||||
|
MaxValue: 5, |
||||
|
Required: true, |
||||
|
DisplayName: "Max Concurrent Tasks", |
||||
|
Description: "Maximum number of balance tasks that can run simultaneously", |
||||
|
Unit: config.UnitCount, |
||||
|
InputType: "number", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "min_server_count", |
||||
|
JSONName: "min_server_count", |
||||
|
Type: config.FieldTypeInt, |
||||
|
DefaultValue: 3, |
||||
|
MinValue: 2, |
||||
|
MaxValue: 20, |
||||
|
Required: true, |
||||
|
DisplayName: "Minimum Server Count", |
||||
|
Description: "Only balance when at least this many servers are available", |
||||
|
Unit: config.UnitCount, |
||||
|
InputType: "number", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// GetErasureCodingTaskConfigSchema returns the schema for erasure coding task configuration
|
||||
|
func GetErasureCodingTaskConfigSchema() *TaskConfigSchema { |
||||
|
return &TaskConfigSchema{ |
||||
|
TaskName: "erasure_coding", |
||||
|
DisplayName: "Erasure Coding", |
||||
|
Description: "Converts volumes to erasure coded format for improved data durability", |
||||
|
Icon: "fas fa-shield-alt text-info", |
||||
|
Schema: config.Schema{ |
||||
|
Fields: []*config.Field{ |
||||
|
{ |
||||
|
Name: "enabled", |
||||
|
JSONName: "enabled", |
||||
|
Type: config.FieldTypeBool, |
||||
|
DefaultValue: true, |
||||
|
Required: false, |
||||
|
DisplayName: "Enable Erasure Coding Tasks", |
||||
|
Description: "Whether erasure coding tasks should be automatically created", |
||||
|
InputType: "checkbox", |
||||
|
CSSClasses: "form-check-input", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "quiet_for_seconds", |
||||
|
JSONName: "quiet_for_seconds", |
||||
|
Type: config.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: "Quiet For Duration", |
||||
|
Description: "Only apply erasure coding to volumes that have not been modified for this duration", |
||||
|
Unit: config.UnitDays, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "scan_interval_seconds", |
||||
|
JSONName: "scan_interval_seconds", |
||||
|
Type: config.FieldTypeInterval, |
||||
|
DefaultValue: 12 * 60 * 60, // 12 hours
|
||||
|
MinValue: 2 * 60 * 60, // 2 hours
|
||||
|
MaxValue: 24 * 60 * 60, // 24 hours
|
||||
|
Required: true, |
||||
|
DisplayName: "Scan Interval", |
||||
|
Description: "How often to scan for volumes needing erasure coding", |
||||
|
Unit: config.UnitHours, |
||||
|
InputType: "interval", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "max_concurrent", |
||||
|
JSONName: "max_concurrent", |
||||
|
Type: config.FieldTypeInt, |
||||
|
DefaultValue: 1, |
||||
|
MinValue: 1, |
||||
|
MaxValue: 3, |
||||
|
Required: true, |
||||
|
DisplayName: "Max Concurrent Tasks", |
||||
|
Description: "Maximum number of erasure coding tasks that can run simultaneously", |
||||
|
Unit: config.UnitCount, |
||||
|
InputType: "number", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "fullness_ratio", |
||||
|
JSONName: "fullness_ratio", |
||||
|
Type: config.FieldTypeFloat, |
||||
|
DefaultValue: 0.9, // 90%
|
||||
|
MinValue: 0.5, |
||||
|
MaxValue: 1.0, |
||||
|
Required: true, |
||||
|
DisplayName: "Fullness Ratio", |
||||
|
Description: "Only apply erasure coding to volumes with fullness ratio above this threshold", |
||||
|
Placeholder: "0.90 (90%)", |
||||
|
Unit: config.UnitNone, |
||||
|
InputType: "number", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
{ |
||||
|
Name: "collection_filter", |
||||
|
JSONName: "collection_filter", |
||||
|
Type: config.FieldTypeString, |
||||
|
DefaultValue: "", |
||||
|
Required: false, |
||||
|
DisplayName: "Collection Filter", |
||||
|
Description: "Only apply erasure coding to volumes in these collections (comma-separated, leave empty for all)", |
||||
|
Placeholder: "collection1,collection2", |
||||
|
InputType: "text", |
||||
|
CSSClasses: "form-control", |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue