task_config_schema.templ 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. package app
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "fmt"
  6. "reflect"
  7. "strings"
  8. "github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
  9. "github.com/seaweedfs/seaweedfs/weed/worker/tasks"
  10. "github.com/seaweedfs/seaweedfs/weed/admin/config"
  11. "github.com/seaweedfs/seaweedfs/weed/admin/view/components"
  12. "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
  13. )
  14. // Helper function to convert task schema to JSON string
  15. func taskSchemaToJSON(schema *tasks.TaskConfigSchema) string {
  16. if schema == nil {
  17. return "{}"
  18. }
  19. data := map[string]interface{}{
  20. "fields": schema.Fields,
  21. }
  22. jsonBytes, err := json.Marshal(data)
  23. if err != nil {
  24. return "{}"
  25. }
  26. return string(jsonBytes)
  27. }
  28. // Helper function to base64 encode the JSON to avoid HTML escaping issues
  29. func taskSchemaToBase64JSON(schema *tasks.TaskConfigSchema) string {
  30. jsonStr := taskSchemaToJSON(schema)
  31. return base64.StdEncoding.EncodeToString([]byte(jsonStr))
  32. }
  33. templ TaskConfigSchema(data *maintenance.TaskConfigData, schema *tasks.TaskConfigSchema, config interface{}) {
  34. <div class="container-fluid">
  35. <div class="row mb-4">
  36. <div class="col-12">
  37. <div class="d-flex justify-content-between align-items-center">
  38. <h2 class="mb-0">
  39. <i class={schema.Icon + " me-2"}></i>
  40. {schema.DisplayName} Configuration
  41. </h2>
  42. <div class="btn-group">
  43. <a href="/maintenance/config" class="btn btn-outline-secondary">
  44. <i class="fas fa-arrow-left me-1"></i>
  45. Back to System Config
  46. </a>
  47. </div>
  48. </div>
  49. </div>
  50. </div>
  51. <!-- Configuration Card -->
  52. <div class="row">
  53. <div class="col-12">
  54. <div class="card">
  55. <div class="card-header">
  56. <h5 class="mb-0">
  57. <i class="fas fa-cogs me-2"></i>
  58. Task Configuration
  59. </h5>
  60. <p class="mb-0 text-muted small">{schema.Description}</p>
  61. </div>
  62. <div class="card-body">
  63. <form id="taskConfigForm" method="POST">
  64. <!-- Dynamically render all schema fields in defined order -->
  65. for _, field := range schema.Fields {
  66. @TaskConfigField(field, config)
  67. }
  68. <div class="d-flex gap-2">
  69. <button type="submit" class="btn btn-primary">
  70. <i class="fas fa-save me-1"></i>
  71. Save Configuration
  72. </button>
  73. <button type="button" class="btn btn-secondary" onclick="resetToDefaults()">
  74. <i class="fas fa-undo me-1"></i>
  75. Reset to Defaults
  76. </button>
  77. </div>
  78. </form>
  79. </div>
  80. </div>
  81. </div>
  82. </div>
  83. <!-- Performance Notes Card -->
  84. <div class="row mt-4">
  85. <div class="col-12">
  86. <div class="card">
  87. <div class="card-header">
  88. <h5 class="mb-0">
  89. <i class="fas fa-info-circle me-2"></i>
  90. Important Notes
  91. </h5>
  92. </div>
  93. <div class="card-body">
  94. <div class="alert alert-info" role="alert">
  95. if schema.TaskName == "vacuum" {
  96. <h6 class="alert-heading">Vacuum Operations:</h6>
  97. <p class="mb-2"><strong>Performance:</strong> Vacuum operations are I/O intensive and may impact cluster performance.</p>
  98. <p class="mb-2"><strong>Safety:</strong> Only volumes meeting age and garbage thresholds will be processed.</p>
  99. <p class="mb-0"><strong>Recommendation:</strong> Monitor cluster load and adjust concurrent limits accordingly.</p>
  100. } else if schema.TaskName == "balance" {
  101. <h6 class="alert-heading">Balance Operations:</h6>
  102. <p class="mb-2"><strong>Performance:</strong> Volume balancing involves data movement and can impact cluster performance.</p>
  103. <p class="mb-2"><strong>Safety:</strong> Requires adequate server count to ensure data safety during moves.</p>
  104. <p class="mb-0"><strong>Recommendation:</strong> Run during off-peak hours to minimize impact on production workloads.</p>
  105. } else if schema.TaskName == "erasure_coding" {
  106. <h6 class="alert-heading">Erasure Coding Operations:</h6>
  107. <p class="mb-2"><strong>Performance:</strong> Erasure coding is CPU and I/O intensive. Consider running during off-peak hours.</p>
  108. <p class="mb-2"><strong>Durability:</strong> With { fmt.Sprintf("%d+%d", erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount) } configuration, can tolerate up to { fmt.Sprintf("%d", erasure_coding.ParityShardsCount) } shard failures.</p>
  109. <p class="mb-0"><strong>Configuration:</strong> Fullness ratio should be between 0.5 and 1.0 (e.g., 0.90 for 90%).</p>
  110. }
  111. </div>
  112. </div>
  113. </div>
  114. </div>
  115. </div>
  116. </div>
  117. <script>
  118. function resetToDefaults() {
  119. if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {
  120. // Reset form fields to their default values
  121. const form = document.getElementById('taskConfigForm');
  122. const schemaFields = window.taskConfigSchema ? window.taskConfigSchema.fields : {};
  123. Object.keys(schemaFields).forEach(fieldName => {
  124. const field = schemaFields[fieldName];
  125. const element = document.getElementById(fieldName);
  126. if (element && field.default_value !== undefined) {
  127. if (field.input_type === 'checkbox') {
  128. element.checked = field.default_value;
  129. } else if (field.input_type === 'interval') {
  130. // Handle interval fields with value and unit
  131. const valueElement = document.getElementById(fieldName + '_value');
  132. const unitElement = document.getElementById(fieldName + '_unit');
  133. if (valueElement && unitElement && field.default_value) {
  134. const defaultSeconds = field.default_value;
  135. const { value, unit } = convertSecondsToTaskIntervalValueUnit(defaultSeconds);
  136. valueElement.value = value;
  137. unitElement.value = unit;
  138. }
  139. } else {
  140. element.value = field.default_value;
  141. }
  142. }
  143. });
  144. }
  145. }
  146. function convertSecondsToTaskIntervalValueUnit(totalSeconds) {
  147. if (totalSeconds === 0) {
  148. return { value: 0, unit: 'minutes' };
  149. }
  150. // Check if it's evenly divisible by days
  151. if (totalSeconds % (24 * 3600) === 0) {
  152. return { value: totalSeconds / (24 * 3600), unit: 'days' };
  153. }
  154. // Check if it's evenly divisible by hours
  155. if (totalSeconds % 3600 === 0) {
  156. return { value: totalSeconds / 3600, unit: 'hours' };
  157. }
  158. // Default to minutes
  159. return { value: totalSeconds / 60, unit: 'minutes' };
  160. }
  161. // Store schema data for JavaScript access (moved to after div is created)
  162. </script>
  163. <!-- Hidden element to store schema data -->
  164. <div data-task-schema={ taskSchemaToBase64JSON(schema) } style="display: none;"></div>
  165. <script>
  166. // Load schema data now that the div exists
  167. const base64Data = document.querySelector('[data-task-schema]').getAttribute('data-task-schema');
  168. const jsonStr = atob(base64Data);
  169. window.taskConfigSchema = JSON.parse(jsonStr);
  170. </script>
  171. }
  172. // TaskConfigField renders a single task configuration field based on schema with typed field lookup
  173. templ TaskConfigField(field *config.Field, config interface{}) {
  174. if field.InputType == "interval" {
  175. <!-- Interval field with number input + unit dropdown -->
  176. <div class="mb-3">
  177. <label for={ field.JSONName } class="form-label">
  178. { field.DisplayName }
  179. if field.Required {
  180. <span class="text-danger">*</span>
  181. }
  182. </label>
  183. <div class="input-group">
  184. <input
  185. type="number"
  186. class="form-control"
  187. id={ field.JSONName + "_value" }
  188. name={ field.JSONName + "_value" }
  189. value={ fmt.Sprintf("%.0f", components.ConvertInt32SecondsToDisplayValue(getTaskConfigInt32Field(config, field.JSONName))) }
  190. step="1"
  191. min="1"
  192. if field.Required {
  193. required
  194. }
  195. />
  196. <select
  197. class="form-select"
  198. id={ field.JSONName + "_unit" }
  199. name={ field.JSONName + "_unit" }
  200. style="max-width: 120px;"
  201. if field.Required {
  202. required
  203. }
  204. >
  205. <option
  206. value="minutes"
  207. if components.GetInt32DisplayUnit(getTaskConfigInt32Field(config, field.JSONName)) == "minutes" {
  208. selected
  209. }
  210. >
  211. Minutes
  212. </option>
  213. <option
  214. value="hours"
  215. if components.GetInt32DisplayUnit(getTaskConfigInt32Field(config, field.JSONName)) == "hours" {
  216. selected
  217. }
  218. >
  219. Hours
  220. </option>
  221. <option
  222. value="days"
  223. if components.GetInt32DisplayUnit(getTaskConfigInt32Field(config, field.JSONName)) == "days" {
  224. selected
  225. }
  226. >
  227. Days
  228. </option>
  229. </select>
  230. </div>
  231. if field.Description != "" {
  232. <div class="form-text text-muted">{ field.Description }</div>
  233. }
  234. </div>
  235. } else if field.InputType == "checkbox" {
  236. <!-- Checkbox field -->
  237. <div class="mb-3">
  238. <div class="form-check form-switch">
  239. <input
  240. class="form-check-input"
  241. type="checkbox"
  242. id={ field.JSONName }
  243. name={ field.JSONName }
  244. value="on"
  245. if getTaskConfigBoolField(config, field.JSONName) {
  246. checked
  247. }
  248. />
  249. <label class="form-check-label" for={ field.JSONName }>
  250. <strong>{ field.DisplayName }</strong>
  251. </label>
  252. </div>
  253. if field.Description != "" {
  254. <div class="form-text text-muted">{ field.Description }</div>
  255. }
  256. </div>
  257. } else if field.InputType == "text" {
  258. <!-- Text field -->
  259. <div class="mb-3">
  260. <label for={ field.JSONName } class="form-label">
  261. { field.DisplayName }
  262. if field.Required {
  263. <span class="text-danger">*</span>
  264. }
  265. </label>
  266. <input
  267. type="text"
  268. class="form-control"
  269. id={ field.JSONName }
  270. name={ field.JSONName }
  271. value={ getTaskConfigStringField(config, field.JSONName) }
  272. placeholder={ field.Placeholder }
  273. if field.Required {
  274. required
  275. }
  276. />
  277. if field.Description != "" {
  278. <div class="form-text text-muted">{ field.Description }</div>
  279. }
  280. </div>
  281. } else {
  282. <!-- Number field -->
  283. <div class="mb-3">
  284. <label for={ field.JSONName } class="form-label">
  285. { field.DisplayName }
  286. if field.Required {
  287. <span class="text-danger">*</span>
  288. }
  289. </label>
  290. <input
  291. type="number"
  292. class="form-control"
  293. id={ field.JSONName }
  294. name={ field.JSONName }
  295. value={ fmt.Sprintf("%.6g", getTaskConfigFloatField(config, field.JSONName)) }
  296. placeholder={ field.Placeholder }
  297. if field.MinValue != nil {
  298. min={ fmt.Sprintf("%v", field.MinValue) }
  299. }
  300. if field.MaxValue != nil {
  301. max={ fmt.Sprintf("%v", field.MaxValue) }
  302. }
  303. step={ getTaskNumberStep(field) }
  304. if field.Required {
  305. required
  306. }
  307. />
  308. if field.Description != "" {
  309. <div class="form-text text-muted">{ field.Description }</div>
  310. }
  311. </div>
  312. }
  313. }
  314. // Typed field getters for task configs - avoiding interface{} where possible
  315. func getTaskConfigBoolField(config interface{}, fieldName string) bool {
  316. switch fieldName {
  317. case "enabled":
  318. // Use reflection only for the common 'enabled' field in BaseConfig
  319. if value := getTaskFieldValue(config, fieldName); value != nil {
  320. if boolVal, ok := value.(bool); ok {
  321. return boolVal
  322. }
  323. }
  324. return false
  325. default:
  326. // For other boolean fields, use reflection
  327. if value := getTaskFieldValue(config, fieldName); value != nil {
  328. if boolVal, ok := value.(bool); ok {
  329. return boolVal
  330. }
  331. }
  332. return false
  333. }
  334. }
  335. func getTaskConfigInt32Field(config interface{}, fieldName string) int32 {
  336. switch fieldName {
  337. case "scan_interval_seconds", "max_concurrent":
  338. // Common fields that should be int/int32
  339. if value := getTaskFieldValue(config, fieldName); value != nil {
  340. switch v := value.(type) {
  341. case int32:
  342. return v
  343. case int:
  344. return int32(v)
  345. case int64:
  346. return int32(v)
  347. }
  348. }
  349. return 0
  350. default:
  351. // For other int fields, use reflection
  352. if value := getTaskFieldValue(config, fieldName); value != nil {
  353. switch v := value.(type) {
  354. case int32:
  355. return v
  356. case int:
  357. return int32(v)
  358. case int64:
  359. return int32(v)
  360. case float64:
  361. return int32(v)
  362. }
  363. }
  364. return 0
  365. }
  366. }
  367. func getTaskConfigFloatField(config interface{}, fieldName string) float64 {
  368. if value := getTaskFieldValue(config, fieldName); value != nil {
  369. switch v := value.(type) {
  370. case float64:
  371. return v
  372. case float32:
  373. return float64(v)
  374. case int:
  375. return float64(v)
  376. case int32:
  377. return float64(v)
  378. case int64:
  379. return float64(v)
  380. }
  381. }
  382. return 0.0
  383. }
  384. func getTaskConfigStringField(config interface{}, fieldName string) string {
  385. if value := getTaskFieldValue(config, fieldName); value != nil {
  386. if strVal, ok := value.(string); ok {
  387. return strVal
  388. }
  389. // Convert numbers to strings for form display
  390. switch v := value.(type) {
  391. case int:
  392. return fmt.Sprintf("%d", v)
  393. case int32:
  394. return fmt.Sprintf("%d", v)
  395. case int64:
  396. return fmt.Sprintf("%d", v)
  397. case float64:
  398. return fmt.Sprintf("%.6g", v)
  399. case float32:
  400. return fmt.Sprintf("%.6g", v)
  401. }
  402. }
  403. return ""
  404. }
  405. func getTaskNumberStep(field *config.Field) string {
  406. if field.Type == config.FieldTypeFloat {
  407. return "0.01"
  408. }
  409. return "1"
  410. }
  411. func getTaskFieldValue(config interface{}, fieldName string) interface{} {
  412. if config == nil {
  413. return nil
  414. }
  415. // Use reflection to get the field value from the config struct
  416. configValue := reflect.ValueOf(config)
  417. if configValue.Kind() == reflect.Ptr {
  418. configValue = configValue.Elem()
  419. }
  420. if configValue.Kind() != reflect.Struct {
  421. return nil
  422. }
  423. configType := configValue.Type()
  424. for i := 0; i < configValue.NumField(); i++ {
  425. field := configValue.Field(i)
  426. fieldType := configType.Field(i)
  427. // Handle embedded structs recursively (before JSON tag check)
  428. if field.Kind() == reflect.Struct && fieldType.Anonymous {
  429. if value := getTaskFieldValue(field.Interface(), fieldName); value != nil {
  430. return value
  431. }
  432. continue
  433. }
  434. // Get JSON tag name
  435. jsonTag := fieldType.Tag.Get("json")
  436. if jsonTag == "" {
  437. continue
  438. }
  439. // Remove options like ",omitempty"
  440. if commaIdx := strings.Index(jsonTag, ","); commaIdx > 0 {
  441. jsonTag = jsonTag[:commaIdx]
  442. }
  443. // Check if this is the field we're looking for
  444. if jsonTag == fieldName {
  445. return field.Interface()
  446. }
  447. }
  448. return nil
  449. }