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 |
|||
|
|||
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 ( |
|||
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 ( |
|||
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
|
|||
type MaintenanceConfigSchema struct { |
|||
Fields map[string]*ConfigField `json:"fields"` |
|||
config.Schema // Embed common schema functionality
|
|||
} |
|||
|
|||
// GetMaintenanceConfigSchema returns the schema for maintenance configuration
|
|||
func GetMaintenanceConfigSchema() *MaintenanceConfigSchema { |
|||
return &MaintenanceConfigSchema{ |
|||
Fields: map[string]*ConfigField{ |
|||
"enabled": { |
|||
Name: "enabled", |
|||
JSONName: "enabled", |
|||
Type: FieldTypeBool, |
|||
DefaultValue: false, |
|||
Required: false, |
|||
DisplayName: "Enable Maintenance System", |
|||
Description: "When enabled, the system will automatically scan for and execute maintenance tasks", |
|||
HelpText: "Toggle this to enable or disable the entire maintenance system", |
|||
InputType: "checkbox", |
|||
CSSClasses: "form-check-input", |
|||
}, |
|||
"scan_interval_seconds": { |
|||
Name: "scan_interval_seconds", |
|||
JSONName: "scan_interval_seconds", |
|||
Type: FieldTypeInterval, |
|||
DefaultValue: 30 * 60, // 30 minutes in seconds
|
|||
MinValue: 1 * 60, // 1 minute
|
|||
MaxValue: 24 * 60 * 60, // 24 hours
|
|||
Required: true, |
|||
DisplayName: "Scan Interval", |
|||
Description: "How often to scan for maintenance tasks", |
|||
HelpText: "The system will check for new maintenance tasks at this interval", |
|||
Placeholder: "30", |
|||
Unit: UnitMinutes, |
|||
InputType: "interval", |
|||
CSSClasses: "form-control", |
|||
}, |
|||
"worker_timeout_seconds": { |
|||
Name: "worker_timeout_seconds", |
|||
JSONName: "worker_timeout_seconds", |
|||
Type: FieldTypeInterval, |
|||
DefaultValue: 5 * 60, // 5 minutes
|
|||
MinValue: 1 * 60, // 1 minute
|
|||
MaxValue: 60 * 60, // 1 hour
|
|||
Required: true, |
|||
DisplayName: "Worker Timeout", |
|||
Description: "How long to wait for worker heartbeat before considering it inactive", |
|||
HelpText: "Workers that don't send heartbeats within this time are considered offline", |
|||
Placeholder: "5", |
|||
Unit: UnitMinutes, |
|||
InputType: "interval", |
|||
CSSClasses: "form-control", |
|||
}, |
|||
"task_timeout_seconds": { |
|||
Name: "task_timeout_seconds", |
|||
JSONName: "task_timeout_seconds", |
|||
Type: FieldTypeInterval, |
|||
DefaultValue: 2 * 60 * 60, // 2 hours
|
|||
MinValue: 1 * 60 * 60, // 1 hour
|
|||
MaxValue: 24 * 60 * 60, // 24 hours
|
|||
Required: true, |
|||
DisplayName: "Task Timeout", |
|||
Description: "Maximum time allowed for a single task to complete", |
|||
HelpText: "Tasks that run longer than this will be considered failed and may be retried", |
|||
Placeholder: "2", |
|||
Unit: UnitHours, |
|||
InputType: "interval", |
|||
CSSClasses: "form-control", |
|||
}, |
|||
"retry_delay_seconds": { |
|||
Name: "retry_delay_seconds", |
|||
JSONName: "retry_delay_seconds", |
|||
Type: FieldTypeInterval, |
|||
DefaultValue: 15 * 60, // 15 minutes
|
|||
MinValue: 1 * 60, // 1 minute
|
|||
MaxValue: 2 * 60 * 60, // 2 hours
|
|||
Required: true, |
|||
DisplayName: "Retry Delay", |
|||
Description: "Time to wait before retrying failed tasks", |
|||
HelpText: "Failed tasks will wait this long before being retried", |
|||
Placeholder: "15", |
|||
Unit: UnitMinutes, |
|||
InputType: "interval", |
|||
CSSClasses: "form-control", |
|||
}, |
|||
"max_retries": { |
|||
Name: "max_retries", |
|||
JSONName: "max_retries", |
|||
Type: FieldTypeInt, |
|||
DefaultValue: 3, |
|||
MinValue: 0, |
|||
MaxValue: 10, |
|||
Required: false, |
|||
DisplayName: "Default Max Retries", |
|||
Description: "Default number of times to retry failed tasks", |
|||
HelpText: "Tasks will be retried this many times before being marked as permanently failed", |
|||
Placeholder: "3 (default)", |
|||
Unit: UnitCount, |
|||
InputType: "number", |
|||
CSSClasses: "form-control", |
|||
}, |
|||
"cleanup_interval_seconds": { |
|||
Name: "cleanup_interval_seconds", |
|||
JSONName: "cleanup_interval_seconds", |
|||
Type: FieldTypeInterval, |
|||
DefaultValue: 24 * 60 * 60, // 24 hours
|
|||
MinValue: 1 * 60 * 60, // 1 hour
|
|||
MaxValue: 7 * 24 * 60 * 60, // 7 days
|
|||
Required: true, |
|||
DisplayName: "Cleanup Interval", |
|||
Description: "How often to clean up old task records", |
|||
HelpText: "The system will remove old completed/failed tasks at this interval", |
|||
Placeholder: "24", |
|||
Unit: UnitHours, |
|||
InputType: "interval", |
|||
CSSClasses: "form-control", |
|||
}, |
|||
"task_retention_seconds": { |
|||
Name: "task_retention_seconds", |
|||
JSONName: "task_retention_seconds", |
|||
Type: FieldTypeInterval, |
|||
DefaultValue: 7 * 24 * 60 * 60, // 7 days
|
|||
MinValue: 1 * 24 * 60 * 60, // 1 day
|
|||
MaxValue: 30 * 24 * 60 * 60, // 30 days
|
|||
Required: true, |
|||
DisplayName: "Task Retention", |
|||
Description: "How long to keep completed/failed task records", |
|||
HelpText: "Task records older than this will be automatically deleted", |
|||
Placeholder: "7", |
|||
Unit: UnitDays, |
|||
InputType: "interval", |
|||
CSSClasses: "form-control", |
|||
}, |
|||
"global_max_concurrent": { |
|||
Name: "global_max_concurrent", |
|||
JSONName: "global_max_concurrent", |
|||
Type: FieldTypeInt, |
|||
DefaultValue: 4, |
|||
MinValue: 1, |
|||
MaxValue: 20, |
|||
Required: true, |
|||
DisplayName: "Global Concurrent Limit", |
|||
Description: "Maximum number of maintenance tasks that can run simultaneously across all workers", |
|||
HelpText: "This limits the total system load from maintenance operations", |
|||
Placeholder: "4 (default)", |
|||
Unit: UnitCount, |
|||
InputType: "number", |
|||
CSSClasses: "form-control", |
|||
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