You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							381 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							381 lines
						
					
					
						
							14 KiB
						
					
					
				| package app | |
| 
 | |
| import ( | |
|     "fmt" | |
|     "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" | |
|     "github.com/seaweedfs/seaweedfs/weed/admin/config" | |
|     "github.com/seaweedfs/seaweedfs/weed/admin/view/components" | |
| ) | |
| 
 | |
| templ MaintenanceConfigSchema(data *maintenance.MaintenanceConfigData, schema *maintenance.MaintenanceConfigSchema) { | |
|     <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="fas fa-cogs me-2"></i> | |
|                         Maintenance Configuration | |
|                     </h2> | |
|                     <div class="btn-group"> | |
|                         <a href="/maintenance/tasks" class="btn btn-outline-primary"> | |
|                             <i class="fas fa-tasks me-1"></i> | |
|                             View Tasks | |
|                         </a> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
| 
 | |
|         <div class="row"> | |
|             <div class="col-12"> | |
|                 <div class="card"> | |
|                     <div class="card-header"> | |
|                         <h5 class="mb-0">System Settings</h5> | |
|                     </div> | |
|                     <div class="card-body"> | |
|                         <form id="maintenanceConfigForm"> | |
|                             <!-- Dynamically render all schema fields in order --> | |
|                             for _, field := range schema.Fields { | |
|                                 @ConfigField(field, data.Config) | |
|                             } | |
| 
 | |
|                             <div class="d-flex gap-2"> | |
|                                 <button type="button" class="btn btn-primary" onclick="saveConfiguration()"> | |
|                                     <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> | |
| 
 | |
|         <!-- Task Configuration Cards --> | |
|         <div class="row mt-4"> | |
|             <div class="col-md-4"> | |
|                 <div class="card"> | |
|                     <div class="card-header"> | |
|                         <h5 class="mb-0"> | |
|                             <i class="fas fa-broom me-2"></i> | |
|                             Volume Vacuum | |
|                         </h5> | |
|                     </div> | |
|                     <div class="card-body"> | |
|                         <p class="card-text">Reclaims disk space by removing deleted files from volumes.</p> | |
|                         <a href="/maintenance/config/vacuum" class="btn btn-primary">Configure</a> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|             <div class="col-md-4"> | |
|                 <div class="card"> | |
|                     <div class="card-header"> | |
|                         <h5 class="mb-0"> | |
|                             <i class="fas fa-balance-scale me-2"></i> | |
|                             Volume Balance | |
|                         </h5> | |
|                     </div> | |
|                     <div class="card-body"> | |
|                         <p class="card-text">Redistributes volumes across servers to optimize storage utilization.</p> | |
|                         <a href="/maintenance/config/balance" class="btn btn-primary">Configure</a> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|             <div class="col-md-4"> | |
|                 <div class="card"> | |
|                     <div class="card-header"> | |
|                         <h5 class="mb-0"> | |
|                             <i class="fas fa-shield-alt me-2"></i> | |
|                             Erasure Coding | |
|                         </h5> | |
|                     </div> | |
|                     <div class="card-body"> | |
|                         <p class="card-text">Converts volumes to erasure coded format for improved durability.</p> | |
|                         <a href="/maintenance/config/erasure_coding" class="btn btn-primary">Configure</a> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <script> | |
|         function saveConfiguration() { | |
|             const form = document.getElementById('maintenanceConfigForm'); | |
|             const formData = new FormData(form); | |
|              | |
|             // Convert form data to JSON, handling interval fields specially | |
|             const config = {}; | |
|              | |
|             for (let [key, value] of formData.entries()) { | |
|                 if (key.endsWith('_value')) { | |
|                     // This is an interval value part | |
|                     const baseKey = key.replace('_value', ''); | |
|                     const unitKey = baseKey + '_unit'; | |
|                     const unitValue = formData.get(unitKey); | |
|                      | |
|                     if (unitValue) { | |
|                         // Convert to seconds based on unit | |
|                         const numValue = parseInt(value) || 0; | |
|                         let seconds = numValue; | |
|                         switch(unitValue) { | |
|                             case 'minutes': | |
|                                 seconds = numValue * 60; | |
|                                 break; | |
|                             case 'hours': | |
|                                 seconds = numValue * 3600; | |
|                                 break; | |
|                             case 'days': | |
|                                 seconds = numValue * 24 * 3600; | |
|                                 break; | |
|                         } | |
|                         config[baseKey] = seconds; | |
|                     } | |
|                 } else if (key.endsWith('_unit')) { | |
|                     // Skip unit keys - they're handled with their corresponding value | |
|                     continue; | |
|                 } else { | |
|                     // Regular field | |
|                     if (form.querySelector(`[name="${key}"]`).type === 'checkbox') { | |
|                         config[key] = form.querySelector(`[name="${key}"]`).checked; | |
|                     } else { | |
|                         const numValue = parseFloat(value); | |
|                         config[key] = isNaN(numValue) ? value : numValue; | |
|                     } | |
|                 } | |
|             } | |
| 
 | |
|             fetch('/api/maintenance/config', { | |
|                 method: 'PUT', | |
|                 headers: { | |
|                     'Content-Type': 'application/json', | |
|                 }, | |
|                 body: JSON.stringify(config) | |
|             }) | |
|             .then(response => { | |
|                 if (response.status === 401) { | |
|                     alert('Authentication required. Please log in first.'); | |
|                     window.location.href = '/login'; | |
|                     return; | |
|                 } | |
|                 return response.json(); | |
|             }) | |
|             .then(data => { | |
|                 if (!data) return; // Skip if redirected to login | |
|                 if (data.success) { | |
|                     alert('Configuration saved successfully!'); | |
|                     location.reload(); | |
|                 } else { | |
|                     alert('Error saving configuration: ' + (data.error || 'Unknown error')); | |
|                 } | |
|             }) | |
|             .catch(error => { | |
|                 console.error('Error:', error); | |
|                 alert('Error saving configuration: ' + error.message); | |
|             }); | |
|         } | |
| 
 | |
|         function resetToDefaults() { | |
|             if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) { | |
|                 fetch('/maintenance/config/defaults', { | |
|                     method: 'POST', | |
|                     headers: { | |
|                         'Content-Type': 'application/json', | |
|                     } | |
|                 }) | |
|                 .then(response => response.json()) | |
|                 .then(data => { | |
|                     if (data.success) { | |
|                         alert('Configuration reset to defaults!'); | |
|                         location.reload(); | |
|                     } else { | |
|                         alert('Error resetting configuration: ' + (data.error || 'Unknown error')); | |
|                     } | |
|                 }) | |
|                 .catch(error => { | |
|                     console.error('Error:', error); | |
|                     alert('Error resetting configuration: ' + error.message); | |
|                 }); | |
|             } | |
|         } | |
|     </script> | |
| } | |
| 
 | |
| // ConfigField renders a single configuration field based on schema with typed value lookup | |
| templ ConfigField(field *config.Field, config *maintenance.MaintenanceConfig) { | |
|     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", components.ConvertInt32SecondsToDisplayValue(getMaintenanceInt32Field(config, field.JSONName))) } | |
|                     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 components.GetInt32DisplayUnit(getMaintenanceInt32Field(config, field.JSONName)) == "minutes" { | |
|                             selected | |
|                         } | |
|                     > | |
|                         Minutes | |
|                     </option> | |
|                     <option  | |
|                         value="hours" | |
|                         if components.GetInt32DisplayUnit(getMaintenanceInt32Field(config, field.JSONName)) == "hours" { | |
|                             selected | |
|                         } | |
|                     > | |
|                         Hours | |
|                     </option> | |
|                     <option  | |
|                         value="days" | |
|                         if components.GetInt32DisplayUnit(getMaintenanceInt32Field(config, field.JSONName)) == "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 getMaintenanceBoolField(config, field.JSONName) { | |
|                         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 { | |
|         <!-- 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("%d", getMaintenanceInt32Field(config, field.JSONName)) } | |
|                 placeholder={ field.Placeholder } | |
|                 if field.MinValue != nil { | |
|                     min={ fmt.Sprintf("%v", field.MinValue) } | |
|                 } | |
|                 if field.MaxValue != nil { | |
|                     max={ fmt.Sprintf("%v", field.MaxValue) } | |
|                 } | |
|                 step={ getNumberStep(field) } | |
|                 if field.Required { | |
|                     required | |
|                 } | |
|             /> | |
|             if field.Description != "" { | |
|                 <div class="form-text text-muted">{ field.Description }</div> | |
|             } | |
|         </div> | |
|     } | |
| } | |
| 
 | |
| // Helper functions for form field types | |
| 
 | |
| func getNumberStep(field *config.Field) string { | |
|     if field.Type == config.FieldTypeFloat { | |
|         return "0.01" | |
|     } | |
|     return "1" | |
| } | |
| 
 | |
| // Typed field getters for MaintenanceConfig - no interface{} needed | |
| func getMaintenanceInt32Field(config *maintenance.MaintenanceConfig, fieldName string) int32 { | |
|     if config == nil { | |
|         return 0 | |
|     } | |
|      | |
|     switch fieldName { | |
|     case "scan_interval_seconds": | |
|         return config.ScanIntervalSeconds | |
|     case "worker_timeout_seconds": | |
|         return config.WorkerTimeoutSeconds | |
|     case "task_timeout_seconds": | |
|         return config.TaskTimeoutSeconds | |
|     case "retry_delay_seconds": | |
|         return config.RetryDelaySeconds | |
|     case "max_retries": | |
|         return config.MaxRetries | |
|     case "cleanup_interval_seconds": | |
|         return config.CleanupIntervalSeconds | |
|     case "task_retention_seconds": | |
|         return config.TaskRetentionSeconds | |
|     case "global_max_concurrent": | |
|         if config.Policy != nil { | |
|             return config.Policy.GlobalMaxConcurrent | |
|         } | |
|         return 0 | |
|     default: | |
|         return 0 | |
|     } | |
| } | |
| 
 | |
| func getMaintenanceBoolField(config *maintenance.MaintenanceConfig, fieldName string) bool { | |
|     if config == nil { | |
|         return false | |
|     } | |
|      | |
|     switch fieldName { | |
|     case "enabled": | |
|         return config.Enabled | |
|     default: | |
|         return false | |
|     } | |
| } | |
| 
 | |
| // Helper function to convert schema to JSON for JavaScript | |
| templ schemaToJSON(schema *maintenance.MaintenanceConfigSchema) { | |
|     {`{}`} | |
| }  |