aboutsummaryrefslogtreecommitdiff
path: root/weed/admin/view/app/maintenance_config_schema.templ
diff options
context:
space:
mode:
Diffstat (limited to 'weed/admin/view/app/maintenance_config_schema.templ')
-rw-r--r--weed/admin/view/app/maintenance_config_schema.templ381
1 files changed, 381 insertions, 0 deletions
diff --git a/weed/admin/view/app/maintenance_config_schema.templ b/weed/admin/view/app/maintenance_config_schema.templ
new file mode 100644
index 000000000..ee89cab64
--- /dev/null
+++ b/weed/admin/view/app/maintenance_config_schema.templ
@@ -0,0 +1,381 @@
+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) {
+ {`{}`}
+} \ No newline at end of file