Browse Source

interval field UI component

worker-execute-ec-tasks
chrislu 4 months ago
parent
commit
019de6dc7a
  1. 72
      weed/admin/handlers/maintenance_handlers.go
  2. 17
      weed/worker/tasks/balance/ui.go
  3. 97
      weed/worker/tasks/erasure_coding/ec_detector.go
  4. 138
      weed/worker/tasks/erasure_coding/ui.go
  5. 42
      weed/worker/tasks/vacuum/ui.go
  6. 87
      weed/worker/types/task_ui.go

72
weed/admin/handlers/maintenance_handlers.go

@ -8,7 +8,6 @@ import (
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
"github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
"github.com/seaweedfs/seaweedfs/weed/admin/view/components"
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/worker/tasks"
@ -124,75 +123,28 @@ func (h *MaintenanceHandlers) ShowTaskConfig(c *gin.Context) {
return
}
// Try to get templ UI provider first - temporarily disabled
// templUIProvider := getTemplUIProvider(taskType)
var configSections []components.ConfigSectionData
// Temporarily disabled templ UI provider
// if templUIProvider != nil {
// // Use the new templ-based UI provider
// currentConfig := templUIProvider.GetCurrentConfig()
// sections, err := templUIProvider.RenderConfigSections(currentConfig)
// if err != nil {
// c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render configuration sections: " + err.Error()})
// return
// }
// configSections = sections
// } else {
// Fallback to basic configuration for providers that haven't been migrated yet
configSections = []components.ConfigSectionData{
{
Title: "Configuration Settings",
Icon: "fas fa-cogs",
Description: "Configure task detection and scheduling parameters",
Fields: []interface{}{
components.CheckboxFieldData{
FormFieldData: components.FormFieldData{
Name: "enabled",
Label: "Enable Task",
Description: "Whether this task type should be enabled",
},
Checked: true,
},
components.NumberFieldData{
FormFieldData: components.FormFieldData{
Name: "max_concurrent",
Label: "Max Concurrent Tasks",
Description: "Maximum number of concurrent tasks",
Required: true,
},
Value: 2,
Step: "1",
Min: floatPtr(1),
},
components.DurationFieldData{
FormFieldData: components.FormFieldData{
Name: "scan_interval",
Label: "Scan Interval",
Description: "How often to scan for tasks",
Required: true,
},
Value: "30m",
},
},
},
// Get current configuration and render form using the actual UI provider
currentConfig := provider.GetCurrentConfig()
formHTML, err := provider.RenderConfigForm(currentConfig)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render configuration form: " + err.Error()})
return
}
// } // End of disabled templ UI provider else block
// Create task configuration data using templ components
configData := &app.TaskConfigTemplData{
// Create task configuration data using the actual form HTML
configData := &maintenance.TaskConfigData{
TaskType: taskType,
TaskName: provider.GetDisplayName(),
TaskIcon: provider.GetIcon(),
Description: provider.GetDescription(),
ConfigSections: configSections,
ConfigFormHTML: formHTML,
}
// Render HTML template using templ components
// Render HTML template
c.Header("Content-Type", "text/html")
taskConfigComponent := app.TaskConfigTempl(configData)
taskConfigComponent := app.TaskConfig(configData)
layoutComponent := layout.Layout(c, taskConfigComponent)
err := layoutComponent.Render(c.Request.Context(), c.Writer)
err = layoutComponent.Render(c.Request.Context(), c.Writer)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
return

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

@ -104,7 +104,7 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML
true,
)
form.AddDurationField("scan_interval", "Scan Interval", "How often to scan for imbalanced volumes", secondsToDuration(config.ScanIntervalSeconds), true)
form.AddIntervalField("scan_interval", "Scan Interval", "How often to scan for imbalanced volumes", config.ScanIntervalSeconds, true)
// Scheduling Settings
form.AddNumberField(
@ -212,12 +212,19 @@ func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}
}
// Parse scan interval
if values, ok := formData["scan_interval"]; ok && len(values) > 0 {
duration, err := time.ParseDuration(values[0])
if values, ok := formData["scan_interval_value"]; ok && len(values) > 0 {
value, err := strconv.Atoi(values[0])
if err != nil {
return nil, fmt.Errorf("invalid scan interval: %w", err)
return nil, fmt.Errorf("invalid scan interval value: %w", err)
}
unit := "minute" // default
if units, ok := formData["scan_interval_unit"]; ok && len(units) > 0 {
unit = units[0]
}
config.ScanIntervalSeconds = int(duration.Seconds())
// Convert to seconds
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
}
// Parse max concurrent

97
weed/worker/tasks/erasure_coding/ec_detector.go

@ -2,6 +2,7 @@ package erasure_coding
import (
"fmt"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
@ -10,11 +11,12 @@ import (
// EcDetector implements erasure coding task detection
type EcDetector struct {
enabled bool
volumeAgeHours int
fullnessRatio float64
minSizeMB int // Minimum volume size in MB before considering EC
scanInterval time.Duration
enabled bool
quietForSeconds int
fullnessRatio float64
minSizeMB int // Minimum volume size in MB before considering EC
scanInterval time.Duration
collectionFilter string
}
// Compile-time interface assertions
@ -26,11 +28,12 @@ var (
// NewEcDetector creates a new erasure coding detector with configurable defaults
func NewEcDetector() *EcDetector {
return &EcDetector{
enabled: true, // Enabled for testing
volumeAgeHours: 0, // No age requirement for testing (was 24)
fullnessRatio: 0.8, // 80% full by default
minSizeMB: 50, // Minimum 50MB for testing (was 100MB)
scanInterval: 30 * time.Second, // Faster scanning for testing
enabled: true, // Enabled for testing
quietForSeconds: 0, // No quiet requirement for testing (was 24)
fullnessRatio: 0.90, // 90% full by default
minSizeMB: 50, // Minimum 50MB for testing (was 100MB)
scanInterval: 30 * time.Second, // Faster scanning for testing
collectionFilter: "", // No collection filter by default
}
}
@ -48,11 +51,11 @@ func (d *EcDetector) ScanForTasks(volumeMetrics []*types.VolumeHealthMetrics, cl
var results []*types.TaskDetectionResult
now := time.Now()
ageThreshold := time.Duration(d.volumeAgeHours) * time.Hour
quietThreshold := time.Duration(d.quietForSeconds) * time.Second
minSizeBytes := uint64(d.minSizeMB) * 1024 * 1024
glog.V(2).Infof("EC detector scanning %d volumes with thresholds: age=%dh, fullness=%.2f, minSize=%dMB",
len(volumeMetrics), d.volumeAgeHours, d.fullnessRatio, d.minSizeMB)
glog.V(2).Infof("EC detector scanning %d volumes with thresholds: quietFor=%ds, fullness=%.2f, minSize=%dMB",
len(volumeMetrics), d.quietForSeconds, d.fullnessRatio, d.minSizeMB)
for _, metric := range volumeMetrics {
// Skip if already EC volume
@ -65,8 +68,21 @@ func (d *EcDetector) ScanForTasks(volumeMetrics []*types.VolumeHealthMetrics, cl
continue
}
// Check age and fullness criteria
if metric.Age >= ageThreshold && metric.FullnessRatio >= d.fullnessRatio {
// Check collection filter if specified
if d.collectionFilter != "" {
// Parse comma-separated collections
allowedCollections := make(map[string]bool)
for _, collection := range strings.Split(d.collectionFilter, ",") {
allowedCollections[strings.TrimSpace(collection)] = true
}
// Skip if volume's collection is not in the allowed list
if !allowedCollections[metric.Collection] {
continue
}
}
// Check quiet duration and fullness criteria
if metric.Age >= quietThreshold && metric.FullnessRatio >= d.fullnessRatio {
// Note: Removed read-only requirement for testing
// In production, you might want to enable this:
// if !metric.IsReadOnly {
@ -79,11 +95,11 @@ func (d *EcDetector) ScanForTasks(volumeMetrics []*types.VolumeHealthMetrics, cl
Server: metric.Server,
Collection: metric.Collection,
Priority: types.TaskPriorityLow, // EC is not urgent
Reason: fmt.Sprintf("Volume meets EC criteria: age=%.1fh (>%dh), fullness=%.1f%% (>%.1f%%), size=%.1fMB (>%dMB)",
metric.Age.Hours(), d.volumeAgeHours, metric.FullnessRatio*100, d.fullnessRatio*100,
Reason: fmt.Sprintf("Volume meets EC criteria: quiet for %.1fs (>%ds), fullness=%.1f%% (>%.1f%%), size=%.1fMB (>%dMB)",
metric.Age.Seconds(), d.quietForSeconds, metric.FullnessRatio*100, d.fullnessRatio*100,
float64(metric.Size)/(1024*1024), d.minSizeMB),
Parameters: map[string]interface{}{
"age_hours": int(metric.Age.Hours()),
"age_seconds": int(metric.Age.Seconds()),
"fullness_ratio": metric.FullnessRatio,
"size_mb": int(metric.Size / (1024 * 1024)),
},
@ -117,8 +133,8 @@ func (d *EcDetector) Configure(config map[string]interface{}) error {
d.enabled = enabled
}
if ageHours, ok := config["volume_age_hours"].(float64); ok {
d.volumeAgeHours = int(ageHours)
if ageSeconds, ok := config["quiet_for_seconds"].(float64); ok {
d.quietForSeconds = int(ageSeconds)
}
if fullnessRatio, ok := config["fullness_ratio"].(float64); ok {
@ -129,8 +145,12 @@ func (d *EcDetector) Configure(config map[string]interface{}) error {
d.minSizeMB = int(minSizeMB)
}
glog.V(1).Infof("EC detector configured: enabled=%v, age=%dh, fullness=%.2f, minSize=%dMB",
d.enabled, d.volumeAgeHours, d.fullnessRatio, d.minSizeMB)
if collectionFilter, ok := config["collection_filter"].(string); ok {
d.collectionFilter = collectionFilter
}
glog.V(1).Infof("EC detector configured: enabled=%v, quietFor=%ds, fullness=%.2f, minSize=%dMB, collection_filter='%s'",
d.enabled, d.quietForSeconds, d.fullnessRatio, d.minSizeMB, d.collectionFilter)
return nil
}
@ -141,14 +161,26 @@ func (d *EcDetector) SetEnabled(enabled bool) {
d.enabled = enabled
}
func (d *EcDetector) SetVolumeAgeSeconds(seconds int) {
d.quietForSeconds = seconds
}
func (d *EcDetector) SetVolumeAgeHours(hours int) {
d.volumeAgeHours = hours
d.quietForSeconds = hours * 3600 // Convert hours to seconds
}
func (d *EcDetector) SetQuietForSeconds(seconds int) {
d.quietForSeconds = seconds
}
func (d *EcDetector) SetFullnessRatio(ratio float64) {
d.fullnessRatio = ratio
}
func (d *EcDetector) SetCollectionFilter(filter string) {
d.collectionFilter = filter
}
func (d *EcDetector) SetScanInterval(interval time.Duration) {
d.scanInterval = interval
}
@ -180,9 +212,19 @@ func (d *EcDetector) ConfigureFromPolicy(policy interface{}) {
}
}
// GetVolumeAgeHours returns the current volume age threshold in hours
// GetVolumeAgeSeconds returns the current volume age threshold in seconds (legacy method)
func (d *EcDetector) GetVolumeAgeSeconds() int {
return d.quietForSeconds
}
// GetVolumeAgeHours returns the current volume age threshold in hours (legacy method)
func (d *EcDetector) GetVolumeAgeHours() int {
return d.volumeAgeHours
return d.quietForSeconds / 3600 // Convert seconds to hours
}
// GetQuietForSeconds returns the current quiet duration threshold in seconds
func (d *EcDetector) GetQuietForSeconds() int {
return d.quietForSeconds
}
// GetFullnessRatio returns the current fullness ratio threshold
@ -190,6 +232,11 @@ func (d *EcDetector) GetFullnessRatio() float64 {
return d.fullnessRatio
}
// GetCollectionFilter returns the current collection filter
func (d *EcDetector) GetCollectionFilter() string {
return d.collectionFilter
}
// GetScanInterval returns the scan interval
func (d *EcDetector) GetScanInterval() time.Duration {
return d.scanInterval

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

@ -46,14 +46,12 @@ func (ui *UIProvider) GetIcon() string {
// ErasureCodingConfig represents the erasure coding configuration
type ErasureCodingConfig struct {
Enabled bool `json:"enabled"`
VolumeAgeHoursSeconds int `json:"volume_age_hours_seconds"`
FullnessRatio float64 `json:"fullness_ratio"`
ScanIntervalSeconds int `json:"scan_interval_seconds"`
MaxConcurrent int `json:"max_concurrent"`
ShardCount int `json:"shard_count"`
ParityCount int `json:"parity_count"`
CollectionFilter string `json:"collection_filter"`
Enabled bool `json:"enabled"`
QuietForSeconds int `json:"quiet_for_seconds"`
FullnessRatio float64 `json:"fullness_ratio"`
ScanIntervalSeconds int `json:"scan_interval_seconds"`
MaxConcurrent int `json:"max_concurrent"`
CollectionFilter string `json:"collection_filter"`
}
// Helper functions for duration conversion
@ -95,19 +93,19 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML
config.Enabled,
)
form.AddNumberField(
"volume_age_hours_seconds",
"Volume Age Threshold",
"Only apply erasure coding to volumes older than this duration",
float64(config.VolumeAgeHoursSeconds),
form.AddIntervalField(
"quiet_for_seconds",
"Quiet For Duration",
"Only apply erasure coding to volumes that have not been modified for this duration",
config.QuietForSeconds,
true,
)
form.AddNumberField(
form.AddIntervalField(
"scan_interval_seconds",
"Scan Interval",
"How often to scan for volumes needing erasure coding",
float64(config.ScanIntervalSeconds),
config.ScanIntervalSeconds,
true,
)
@ -120,21 +118,21 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML
true,
)
// Erasure Coding Parameters
// Detection Parameters
form.AddNumberField(
"shard_count",
"Data Shards",
"Number of data shards for erasure coding (recommended: 10)",
float64(config.ShardCount),
"fullness_ratio",
"Fullness Ratio (0.0-1.0)",
"Only apply erasure coding to volumes with fullness ratio above this threshold (e.g., 0.90 for 90%)",
config.FullnessRatio,
true,
)
form.AddNumberField(
"parity_count",
"Parity Shards",
"Number of parity shards for erasure coding (recommended: 4)",
float64(config.ParityCount),
true,
form.AddTextField(
"collection_filter",
"Collection Filter",
"Only apply erasure coding to volumes in these collections (comma-separated, leave empty for all collections)",
config.CollectionFilter,
false,
)
// Generate organized form sections using Bootstrap components
@ -168,7 +166,8 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML
<div class="alert alert-info" role="alert">
<h6 class="alert-heading">Important Notes:</h6>
<p class="mb-2"><strong>Performance:</strong> Erasure coding is CPU and I/O intensive. Consider running during off-peak hours.</p>
<p class="mb-0"><strong>Durability:</strong> With ` + fmt.Sprintf("%d+%d", config.ShardCount, config.ParityCount) + ` configuration, can tolerate up to ` + fmt.Sprintf("%d", config.ParityCount) + ` shard failures.</p>
<p class="mb-2"><strong>Durability:</strong> With 10+4 configuration, can tolerate up to 4 shard failures.</p>
<p class="mb-0"><strong>Configuration:</strong> Use the dropdown to select time units (days, hours, minutes). Fullness ratio should be between 0.0 and 1.0 (e.g., 0.90 for 90%).</p>
</div>
</div>
</div>
@ -180,27 +179,41 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML
// ParseConfigForm parses form data into configuration
func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}, error) {
config := &ErasureCodingConfig{}
config := ErasureCodingConfig{}
// Parse enabled
config.Enabled = len(formData["enabled"]) > 0
// Parse volume age hours
if values, ok := formData["volume_age_hours_seconds"]; ok && len(values) > 0 {
hours, err := strconv.Atoi(values[0])
// Parse quiet for duration
if values, ok := formData["quiet_for_seconds_value"]; ok && len(values) > 0 {
value, err := strconv.Atoi(values[0])
if err != nil {
return nil, fmt.Errorf("invalid volume age hours: %w", err)
return nil, fmt.Errorf("invalid quiet for duration value: %w", err)
}
config.VolumeAgeHoursSeconds = hours
unit := "minute" // default
if units, ok := formData["quiet_for_seconds_unit"]; ok && len(units) > 0 {
unit = units[0]
}
// Convert to seconds using the helper function from types package
config.QuietForSeconds = types.IntervalValueUnitToSeconds(value, unit)
}
// Parse scan interval
if values, ok := formData["scan_interval_seconds"]; ok && len(values) > 0 {
interval, err := strconv.Atoi(values[0])
if values, ok := formData["scan_interval_seconds_value"]; ok && len(values) > 0 {
value, err := strconv.Atoi(values[0])
if err != nil {
return nil, fmt.Errorf("invalid scan interval: %w", err)
return nil, fmt.Errorf("invalid scan interval value: %w", err)
}
config.ScanIntervalSeconds = interval
unit := "minute" // default
if units, ok := formData["scan_interval_seconds_unit"]; ok && len(units) > 0 {
unit = units[0]
}
// Convert to seconds
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
}
// Parse max concurrent
@ -215,28 +228,21 @@ func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}
config.MaxConcurrent = maxConcurrent
}
// Parse shard count
if values, ok := formData["shard_count"]; ok && len(values) > 0 {
shardCount, err := strconv.Atoi(values[0])
// Parse fullness ratio
if values, ok := formData["fullness_ratio"]; ok && len(values) > 0 {
fullnessRatio, err := strconv.ParseFloat(values[0], 64)
if err != nil {
return nil, fmt.Errorf("invalid shard count: %w", err)
return nil, fmt.Errorf("invalid fullness ratio: %w", err)
}
if shardCount < 1 {
return nil, fmt.Errorf("shard count must be at least 1")
if fullnessRatio < 0 || fullnessRatio > 1 {
return nil, fmt.Errorf("fullness ratio must be between 0.0 and 1.0")
}
config.ShardCount = shardCount
config.FullnessRatio = fullnessRatio
}
// Parse parity count
if values, ok := formData["parity_count"]; ok && len(values) > 0 {
parityCount, err := strconv.Atoi(values[0])
if err != nil {
return nil, fmt.Errorf("invalid parity count: %w", err)
}
if parityCount < 1 {
return nil, fmt.Errorf("parity count must be at least 1")
}
config.ParityCount = parityCount
// Parse collection filter
if values, ok := formData["collection_filter"]; ok && len(values) > 0 {
config.CollectionFilter = values[0]
}
return config, nil
@ -257,7 +263,9 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
// Apply to detector
if ui.detector != nil {
ui.detector.SetEnabled(ecConfig.Enabled)
ui.detector.SetVolumeAgeHours(ecConfig.VolumeAgeHoursSeconds)
ui.detector.SetQuietForSeconds(ecConfig.QuietForSeconds)
ui.detector.SetFullnessRatio(ecConfig.FullnessRatio)
ui.detector.SetCollectionFilter(ecConfig.CollectionFilter)
ui.detector.SetScanInterval(secondsToDuration(ecConfig.ScanIntervalSeconds))
}
@ -267,8 +275,8 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
ui.scheduler.SetMaxConcurrent(ecConfig.MaxConcurrent)
}
glog.V(1).Infof("Applied erasure coding configuration: enabled=%v, age_threshold=%v, max_concurrent=%d, shards=%d+%d",
ecConfig.Enabled, ecConfig.VolumeAgeHoursSeconds, ecConfig.MaxConcurrent, ecConfig.ShardCount, ecConfig.ParityCount)
glog.V(1).Infof("Applied erasure coding configuration: enabled=%v, quiet_for=%v seconds, max_concurrent=%d, fullness_ratio=%f, collection_filter=%s, shards=10+4",
ecConfig.Enabled, ecConfig.QuietForSeconds, ecConfig.MaxConcurrent, ecConfig.FullnessRatio, ecConfig.CollectionFilter)
return nil
}
@ -277,18 +285,20 @@ func (ui *UIProvider) ApplyConfig(config interface{}) error {
func (ui *UIProvider) getCurrentECConfig() ErasureCodingConfig {
config := ErasureCodingConfig{
// Default values (fallback if detectors/schedulers are nil)
Enabled: true,
VolumeAgeHoursSeconds: 24 * 3600, // 24 hours in seconds
ScanIntervalSeconds: 2 * 3600, // 2 hours in seconds
MaxConcurrent: 1,
ShardCount: 10,
ParityCount: 4,
Enabled: true,
QuietForSeconds: 24 * 3600, // Default to 24 hours in seconds
ScanIntervalSeconds: 2 * 3600, // 2 hours in seconds
MaxConcurrent: 1,
FullnessRatio: 0.90, // Default fullness ratio
CollectionFilter: "",
}
// Get current values from detector
if ui.detector != nil {
config.Enabled = ui.detector.IsEnabled()
config.VolumeAgeHoursSeconds = ui.detector.GetVolumeAgeHours()
config.QuietForSeconds = ui.detector.GetQuietForSeconds()
config.FullnessRatio = ui.detector.GetFullnessRatio()
config.CollectionFilter = ui.detector.GetCollectionFilter()
config.ScanIntervalSeconds = durationToSeconds(ui.detector.ScanInterval())
}

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

@ -101,19 +101,19 @@ func (ui *UIProvider) RenderConfigForm(currentConfig interface{}) (template.HTML
true,
)
form.AddDurationField(
form.AddIntervalField(
"scan_interval",
"Scan Interval",
"How often to scan for volumes needing vacuum",
secondsToDuration(config.ScanIntervalSeconds),
config.ScanIntervalSeconds,
true,
)
form.AddDurationField(
form.AddIntervalField(
"min_volume_age",
"Minimum Volume Age",
"Only vacuum volumes older than this duration",
secondsToDuration(config.MinVolumeAgeSeconds),
config.MinVolumeAgeSeconds,
true,
)
@ -189,21 +189,35 @@ func (ui *UIProvider) ParseConfigForm(formData map[string][]string) (interface{}
}
// Parse scan interval
if intervalStr := formData["scan_interval"]; len(intervalStr) > 0 {
if interval, err := time.ParseDuration(intervalStr[0]); err != nil {
return nil, fmt.Errorf("invalid scan interval: %w", err)
} else {
config.ScanIntervalSeconds = durationToSeconds(interval)
if values, ok := formData["scan_interval_value"]; ok && len(values) > 0 {
value, err := strconv.Atoi(values[0])
if err != nil {
return nil, fmt.Errorf("invalid scan interval value: %w", err)
}
unit := "minute" // default
if units, ok := formData["scan_interval_unit"]; ok && len(units) > 0 {
unit = units[0]
}
// Convert to seconds
config.ScanIntervalSeconds = types.IntervalValueUnitToSeconds(value, unit)
}
// Parse min volume age
if ageStr := formData["min_volume_age"]; len(ageStr) > 0 {
if age, err := time.ParseDuration(ageStr[0]); err != nil {
return nil, fmt.Errorf("invalid min volume age: %w", err)
} else {
config.MinVolumeAgeSeconds = durationToSeconds(age)
if values, ok := formData["min_volume_age_value"]; ok && len(values) > 0 {
value, err := strconv.Atoi(values[0])
if err != nil {
return nil, fmt.Errorf("invalid min volume age value: %w", err)
}
unit := "minute" // default
if units, ok := formData["min_volume_age_unit"]; ok && len(units) > 0 {
unit = units[0]
}
// Convert to seconds
config.MinVolumeAgeSeconds = types.IntervalValueUnitToSeconds(value, unit)
}
// Parse max concurrent

87
weed/worker/types/task_ui.go

@ -6,6 +6,40 @@ import (
"time"
)
// Helper function to convert seconds to the most appropriate interval unit
func secondsToIntervalValueUnit(totalSeconds int) (int, string) {
if totalSeconds == 0 {
return 0, "minute"
}
// Check if it's evenly divisible by days
if totalSeconds%(24*3600) == 0 {
return totalSeconds / (24 * 3600), "day"
}
// Check if it's evenly divisible by hours
if totalSeconds%3600 == 0 {
return totalSeconds / 3600, "hour"
}
// Default to minutes
return totalSeconds / 60, "minute"
}
// Helper function to convert interval value and unit to seconds
func IntervalValueUnitToSeconds(value int, unit string) int {
switch unit {
case "day":
return value * 24 * 3600
case "hour":
return value * 3600
case "minute":
return value * 60
default:
return value * 60 // Default to minutes
}
}
// TaskUIProvider defines how tasks provide their configuration UI
type TaskUIProvider interface {
// GetTaskType returns the task type
@ -100,11 +134,14 @@ type TaskDetailsData struct {
type FormField struct {
Name string `json:"name"`
Label string `json:"label"`
Type string `json:"type"` // text, number, checkbox, select, duration
Type string `json:"type"` // text, number, checkbox, select, duration, interval
Value interface{} `json:"value"`
Description string `json:"description"`
Required bool `json:"required"`
Options []FormOption `json:"options,omitempty"` // For select fields
// Interval-specific fields
IntervalValue int `json:"interval_value,omitempty"` // Numeric value for interval
IntervalUnit string `json:"interval_unit,omitempty"` // Unit for interval (day, hour, minute)
}
type FormOption struct {
@ -190,6 +227,23 @@ func (fb *FormBuilder) AddDurationField(name, label, description string, value t
return fb
}
// AddIntervalField adds an interval field with value and unit dropdown
func (fb *FormBuilder) AddIntervalField(name, label, description string, totalSeconds int, required bool) *FormBuilder {
// Convert seconds to the most appropriate unit
value, unit := secondsToIntervalValueUnit(totalSeconds)
fb.fields = append(fb.fields, FormField{
Name: name,
Label: label,
Type: "interval",
Description: description,
Required: required,
IntervalValue: value,
IntervalUnit: unit,
})
return fb
}
// Build generates the HTML form fields with Bootstrap styling
func (fb *FormBuilder) Build() template.HTML {
html := ""
@ -269,6 +323,37 @@ func (fb *FormBuilder) renderField(field FormField) string {
html += " required"
}
html += ">\n"
case "interval":
// Create input group with number input and unit select
html += " <div class=\"input-group\">\n"
html += " <input type=\"number\" class=\"form-control\" id=\"" + field.Name + "_value\" name=\"" + field.Name + "_value\" min=\"0\" value=\"" +
fmt.Sprintf("%d", field.IntervalValue) + "\""
if field.Required {
html += " required"
}
html += ">\n"
html += " <select class=\"form-select\" id=\"" + field.Name + "_unit\" name=\"" + field.Name + "_unit\" style=\"max-width: 120px;\""
if field.Required {
html += " required"
}
html += ">\n"
// Add unit options
units := []struct{ Value, Label string }{
{"minute", "Minutes"},
{"hour", "Hours"},
{"day", "Days"},
}
for _, unit := range units {
selected := ""
if unit.Value == field.IntervalUnit {
selected = " selected"
}
html += " <option value=\"" + unit.Value + "\"" + selected + ">" + unit.Label + "</option>\n"
}
html += " </select>\n"
html += " </div>\n"
}
// Description for non-checkbox fields

Loading…
Cancel
Save