mirror of
https://github.com/chrislusf/seaweedfs
synced 2025-09-10 13:22:47 +02:00
* initial design * added simulation as tests * reorganized the codebase to move the simulation framework and tests into their own dedicated package * integration test. ec worker task * remove "enhanced" reference * start master, volume servers, filer Current Status ✅ Master: Healthy and running (port 9333) ✅ Filer: Healthy and running (port 8888) ✅ Volume Servers: All 6 servers running (ports 8080-8085) 🔄 Admin/Workers: Will start when dependencies are ready * generate write load * tasks are assigned * admin start wtih grpc port. worker has its own working directory * Update .gitignore * working worker and admin. Task detection is not working yet. * compiles, detection uses volumeSizeLimitMB from master * compiles * worker retries connecting to admin * build and restart * rendering pending tasks * skip task ID column * sticky worker id * test canScheduleTaskNow * worker reconnect to admin * clean up logs * worker register itself first * worker can run ec work and report status but: 1. one volume should not be repeatedly worked on. 2. ec shards needs to be distributed and source data should be deleted. * move ec task logic * listing ec shards * local copy, ec. Need to distribute. * ec is mostly working now * distribution of ec shards needs improvement * need configuration to enable ec * show ec volumes * interval field UI component * rename * integration test with vauuming * garbage percentage threshold * fix warning * display ec shard sizes * fix ec volumes list * Update ui.go * show default values * ensure correct default value * MaintenanceConfig use ConfigField * use schema defined defaults * config * reduce duplication * refactor to use BaseUIProvider * each task register its schema * checkECEncodingCandidate use ecDetector * use vacuumDetector * use volumeSizeLimitMB * remove remove * remove unused * refactor * use new framework * remove v2 reference * refactor * left menu can scroll now * The maintenance manager was not being initialized when no data directory was configured for persistent storage. * saving config * Update task_config_schema_templ.go * enable/disable tasks * protobuf encoded task configurations * fix system settings * use ui component * remove logs * interface{} Reduction * reduce interface{} * reduce interface{} * avoid from/to map * reduce interface{} * refactor * keep it DRY * added logging * debug messages * debug level * debug * show the log caller line * use configured task policy * log level * handle admin heartbeat response * Update worker.go * fix EC rack and dc count * Report task status to admin server * fix task logging, simplify interface checking, use erasure_coding constants * factor in empty volume server during task planning * volume.list adds disk id * track disk id also * fix locking scheduled and manual scanning * add active topology * simplify task detector * ec task completed, but shards are not showing up * implement ec in ec_typed.go * adjust log level * dedup * implementing ec copying shards and only ecx files * use disk id when distributing ec shards 🎯 Planning: ActiveTopology creates DestinationPlan with specific TargetDisk 📦 Task Creation: maintenance_integration.go creates ECDestination with DiskId 🚀 Task Execution: EC task passes DiskId in VolumeEcShardsCopyRequest 💾 Volume Server: Receives disk_id and stores shards on specific disk (vs.store.Locations[req.DiskId]) 📂 File System: EC shards and metadata land in the exact disk directory planned * Delete original volume from all locations * clean up existing shard locations * local encoding and distributing * Update docker/admin_integration/EC-TESTING-README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * check volume id range * simplify * fix tests * fix types * clean up logs and tests --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
424 lines
No EOL
9.9 KiB
Text
424 lines
No EOL
9.9 KiB
Text
package components
|
|
|
|
import "fmt"
|
|
|
|
// FormFieldData represents common form field data
|
|
type FormFieldData struct {
|
|
Name string
|
|
Label string
|
|
Description string
|
|
Required bool
|
|
}
|
|
|
|
// TextFieldData represents text input field data
|
|
type TextFieldData struct {
|
|
FormFieldData
|
|
Value string
|
|
Placeholder string
|
|
}
|
|
|
|
// NumberFieldData represents number input field data
|
|
type NumberFieldData struct {
|
|
FormFieldData
|
|
Value float64
|
|
Step string
|
|
Min *float64
|
|
Max *float64
|
|
}
|
|
|
|
// CheckboxFieldData represents checkbox field data
|
|
type CheckboxFieldData struct {
|
|
FormFieldData
|
|
Checked bool
|
|
}
|
|
|
|
// SelectFieldData represents select field data
|
|
type SelectFieldData struct {
|
|
FormFieldData
|
|
Value string
|
|
Options []SelectOption
|
|
}
|
|
|
|
type SelectOption struct {
|
|
Value string
|
|
Label string
|
|
}
|
|
|
|
// DurationFieldData represents duration input field data
|
|
type DurationFieldData struct {
|
|
FormFieldData
|
|
Value string
|
|
Placeholder string
|
|
}
|
|
|
|
// DurationInputFieldData represents duration input with number + unit dropdown
|
|
type DurationInputFieldData struct {
|
|
FormFieldData
|
|
Seconds int // The duration value in seconds
|
|
}
|
|
|
|
// TextField renders a Bootstrap text input field
|
|
templ TextField(data TextFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
value={ data.Value }
|
|
if data.Placeholder != "" {
|
|
placeholder={ data.Placeholder }
|
|
}
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// NumberField renders a Bootstrap number input field
|
|
templ NumberField(data NumberFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<input
|
|
type="number"
|
|
class="form-control"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
value={ fmt.Sprintf("%.6g", data.Value) }
|
|
if data.Step != "" {
|
|
step={ data.Step }
|
|
} else {
|
|
step="any"
|
|
}
|
|
if data.Min != nil {
|
|
min={ fmt.Sprintf("%.6g", *data.Min) }
|
|
}
|
|
if data.Max != nil {
|
|
max={ fmt.Sprintf("%.6g", *data.Max) }
|
|
}
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// CheckboxField renders a Bootstrap checkbox field
|
|
templ CheckboxField(data CheckboxFieldData) {
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input
|
|
type="checkbox"
|
|
class="form-check-input"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
if data.Checked {
|
|
checked
|
|
}
|
|
/>
|
|
<label class="form-check-label" for={ data.Name }>
|
|
{ data.Label }
|
|
</label>
|
|
</div>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// SelectField renders a Bootstrap select field
|
|
templ SelectField(data SelectFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<select
|
|
class="form-select"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
if data.Required {
|
|
required
|
|
}
|
|
>
|
|
for _, option := range data.Options {
|
|
<option
|
|
value={ option.Value }
|
|
if option.Value == data.Value {
|
|
selected
|
|
}
|
|
>
|
|
{ option.Label }
|
|
</option>
|
|
}
|
|
</select>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// DurationField renders a Bootstrap duration input field
|
|
templ DurationField(data DurationFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
value={ data.Value }
|
|
if data.Placeholder != "" {
|
|
placeholder={ data.Placeholder }
|
|
} else {
|
|
placeholder="e.g., 30m, 2h, 24h"
|
|
}
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// DurationInputField renders a Bootstrap duration input with number + unit dropdown
|
|
templ DurationInputField(data DurationInputFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<div class="input-group">
|
|
<input
|
|
type="number"
|
|
class="form-control"
|
|
id={ data.Name }
|
|
name={ data.Name }
|
|
value={ fmt.Sprintf("%.0f", convertSecondsToValue(data.Seconds, convertSecondsToUnit(data.Seconds))) }
|
|
step="1"
|
|
min="1"
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
<select
|
|
class="form-select"
|
|
id={ data.Name + "_unit" }
|
|
name={ data.Name + "_unit" }
|
|
style="max-width: 120px;"
|
|
>
|
|
<option
|
|
value="minutes"
|
|
if convertSecondsToUnit(data.Seconds) == "minutes" {
|
|
selected
|
|
}
|
|
>
|
|
Minutes
|
|
</option>
|
|
<option
|
|
value="hours"
|
|
if convertSecondsToUnit(data.Seconds) == "hours" {
|
|
selected
|
|
}
|
|
>
|
|
Hours
|
|
</option>
|
|
<option
|
|
value="days"
|
|
if convertSecondsToUnit(data.Seconds) == "days" {
|
|
selected
|
|
}
|
|
>
|
|
Days
|
|
</option>
|
|
</select>
|
|
</div>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
// Helper functions for duration conversion (used by DurationInputField)
|
|
|
|
// Typed conversion functions for protobuf int32 (most common) - EXPORTED
|
|
func ConvertInt32SecondsToDisplayValue(seconds int32) float64 {
|
|
return convertIntSecondsToDisplayValue(int(seconds))
|
|
}
|
|
|
|
func GetInt32DisplayUnit(seconds int32) string {
|
|
return getIntDisplayUnit(int(seconds))
|
|
}
|
|
|
|
// Typed conversion functions for regular int
|
|
func convertIntSecondsToDisplayValue(seconds int) float64 {
|
|
if seconds == 0 {
|
|
return 0
|
|
}
|
|
|
|
// Check if it's evenly divisible by days
|
|
if seconds%(24*3600) == 0 {
|
|
return float64(seconds / (24 * 3600))
|
|
}
|
|
|
|
// Check if it's evenly divisible by hours
|
|
if seconds%3600 == 0 {
|
|
return float64(seconds / 3600)
|
|
}
|
|
|
|
// Default to minutes
|
|
return float64(seconds / 60)
|
|
}
|
|
|
|
func getIntDisplayUnit(seconds int) string {
|
|
if seconds == 0 {
|
|
return "minutes"
|
|
}
|
|
|
|
// Check if it's evenly divisible by days
|
|
if seconds%(24*3600) == 0 {
|
|
return "days"
|
|
}
|
|
|
|
// Check if it's evenly divisible by hours
|
|
if seconds%3600 == 0 {
|
|
return "hours"
|
|
}
|
|
|
|
// Default to minutes
|
|
return "minutes"
|
|
}
|
|
|
|
func convertSecondsToUnit(seconds int) string {
|
|
if seconds == 0 {
|
|
return "minutes"
|
|
}
|
|
|
|
// Try days first
|
|
if seconds%(24*3600) == 0 && seconds >= 24*3600 {
|
|
return "days"
|
|
}
|
|
|
|
// Try hours
|
|
if seconds%3600 == 0 && seconds >= 3600 {
|
|
return "hours"
|
|
}
|
|
|
|
// Default to minutes
|
|
return "minutes"
|
|
}
|
|
|
|
func convertSecondsToValue(seconds int, unit string) float64 {
|
|
if seconds == 0 {
|
|
return 0
|
|
}
|
|
|
|
switch unit {
|
|
case "days":
|
|
return float64(seconds / (24 * 3600))
|
|
case "hours":
|
|
return float64(seconds / 3600)
|
|
case "minutes":
|
|
return float64(seconds / 60)
|
|
default:
|
|
return float64(seconds / 60) // Default to minutes
|
|
}
|
|
}
|
|
|
|
// IntervalFieldData represents interval input field data with separate value and unit
|
|
type IntervalFieldData struct {
|
|
FormFieldData
|
|
Seconds int // The interval value in seconds
|
|
}
|
|
|
|
// IntervalField renders a Bootstrap interval input with number + unit dropdown (like task config)
|
|
templ IntervalField(data IntervalFieldData) {
|
|
<div class="mb-3">
|
|
<label for={ data.Name } class="form-label">
|
|
{ data.Label }
|
|
if data.Required {
|
|
<span class="text-danger">*</span>
|
|
}
|
|
</label>
|
|
<div class="input-group">
|
|
<input
|
|
type="number"
|
|
class="form-control"
|
|
id={ data.Name + "_value" }
|
|
name={ data.Name + "_value" }
|
|
value={ fmt.Sprintf("%.0f", convertSecondsToValue(data.Seconds, convertSecondsToUnit(data.Seconds))) }
|
|
step="1"
|
|
min="1"
|
|
if data.Required {
|
|
required
|
|
}
|
|
/>
|
|
<select
|
|
class="form-select"
|
|
id={ data.Name + "_unit" }
|
|
name={ data.Name + "_unit" }
|
|
style="max-width: 120px;"
|
|
if data.Required {
|
|
required
|
|
}
|
|
>
|
|
<option
|
|
value="minutes"
|
|
if convertSecondsToUnit(data.Seconds) == "minutes" {
|
|
selected
|
|
}
|
|
>
|
|
Minutes
|
|
</option>
|
|
<option
|
|
value="hours"
|
|
if convertSecondsToUnit(data.Seconds) == "hours" {
|
|
selected
|
|
}
|
|
>
|
|
Hours
|
|
</option>
|
|
<option
|
|
value="days"
|
|
if convertSecondsToUnit(data.Seconds) == "days" {
|
|
selected
|
|
}
|
|
>
|
|
Days
|
|
</option>
|
|
</select>
|
|
</div>
|
|
if data.Description != "" {
|
|
<div class="form-text text-muted">{ data.Description }</div>
|
|
}
|
|
</div>
|
|
} |