Add ETA estimation to cluster backup/restore operations

- Created internal/progress/estimator.go with ETAEstimator component
- Tracks elapsed time and estimates remaining time based on progress
- Enhanced Spinner and LineByLine indicators to display ETA info
- Integrated into BackupCluster and RestoreCluster functions
- Display format: 'Operation | X/Y (Z%) | Elapsed: Xm | ETA: ~Ym remaining'
- Preserves spinner animation while showing progress/time estimates
- Quick Win approach: no historical data storage, just current operation tracking
This commit is contained in:
2025-11-07 13:28:11 +00:00
parent ebb77fb960
commit b3ac5a18df
17 changed files with 233 additions and 24 deletions

View File

@ -15,16 +15,18 @@ type Indicator interface {
Complete(message string)
Fail(message string)
Stop()
SetEstimator(estimator *ETAEstimator)
}
// Spinner creates a spinning progress indicator
type Spinner struct {
writer io.Writer
message string
active bool
frames []string
interval time.Duration
stopCh chan bool
writer io.Writer
message string
active bool
frames []string
interval time.Duration
stopCh chan bool
estimator *ETAEstimator
}
// NewSpinner creates a new spinner progress indicator
@ -51,7 +53,14 @@ func (s *Spinner) Start(message string) {
return
default:
if s.active {
currentFrame := fmt.Sprintf("%s %s", s.frames[i%len(s.frames)], s.message)
displayMsg := s.message
// Add ETA info if estimator is available
if s.estimator != nil {
displayMsg = s.estimator.GetFullStatus(s.message)
}
currentFrame := fmt.Sprintf("%s %s", s.frames[i%len(s.frames)], displayMsg)
if s.message != lastMessage {
// Print new line for new messages
fmt.Fprintf(s.writer, "\n%s", currentFrame)
@ -68,6 +77,11 @@ func (s *Spinner) Start(message string) {
}()
}
// SetEstimator sets an ETA estimator for this spinner
func (s *Spinner) SetEstimator(estimator *ETAEstimator) {
s.estimator = estimator
}
// Update changes the spinner message
func (s *Spinner) Update(message string) {
s.message = message
@ -166,6 +180,11 @@ func (d *Dots) Stop() {
}
}
// SetEstimator is a no-op for dots (doesn't support ETA display)
func (d *Dots) SetEstimator(estimator *ETAEstimator) {
// Dots indicator doesn't support ETA display
}
// ProgressBar creates a visual progress bar
type ProgressBar struct {
writer io.Writer
@ -232,6 +251,11 @@ func (p *ProgressBar) Stop() {
p.active = false
}
// SetEstimator is a no-op for progress bar (has its own progress tracking)
func (p *ProgressBar) SetEstimator(estimator *ETAEstimator) {
// Progress bar has its own progress tracking
}
// render draws the progress bar
func (p *ProgressBar) render() {
if !p.active {
@ -283,10 +307,16 @@ func (s *Static) Stop() {
// No-op for static indicator
}
// SetEstimator is a no-op for static indicator
func (s *Static) SetEstimator(estimator *ETAEstimator) {
// Static indicator doesn't support ETA display
}
// LineByLine creates a line-by-line progress indicator
type LineByLine struct {
writer io.Writer
silent bool
writer io.Writer
silent bool
estimator *ETAEstimator
}
// NewLineByLine creates a new line-by-line progress indicator
@ -321,16 +351,29 @@ func NewQuietLineByLine() *LineByLine {
// Start shows the initial message
func (l *LineByLine) Start(message string) {
fmt.Fprintf(l.writer, "\n🔄 %s\n", message)
displayMsg := message
if l.estimator != nil {
displayMsg = l.estimator.GetFullStatus(message)
}
fmt.Fprintf(l.writer, "\n🔄 %s\n", displayMsg)
}
// Update shows an update message
func (l *LineByLine) Update(message string) {
if !l.silent {
fmt.Fprintf(l.writer, " %s\n", message)
displayMsg := message
if l.estimator != nil {
displayMsg = l.estimator.GetFullStatus(message)
}
fmt.Fprintf(l.writer, " %s\n", displayMsg)
}
}
// SetEstimator sets an ETA estimator for this indicator
func (l *LineByLine) SetEstimator(estimator *ETAEstimator) {
l.estimator = estimator
}
// Complete shows completion message
func (l *LineByLine) Complete(message string) {
fmt.Fprintf(l.writer, "✅ %s\n\n", message)
@ -375,6 +418,11 @@ func (l *Light) Stop() {
// No cleanup needed for light indicator
}
// SetEstimator is a no-op for light indicator
func (l *Light) SetEstimator(estimator *ETAEstimator) {
// Light indicator doesn't support ETA display
}
// NewIndicator creates an appropriate progress indicator based on environment
func NewIndicator(interactive bool, indicatorType string) Indicator {
if !interactive {
@ -405,8 +453,9 @@ func NewNullIndicator() *NullIndicator {
return &NullIndicator{}
}
func (n *NullIndicator) Start(message string) {}
func (n *NullIndicator) Update(message string) {}
func (n *NullIndicator) Complete(message string) {}
func (n *NullIndicator) Fail(message string) {}
func (n *NullIndicator) Stop() {}
func (n *NullIndicator) Start(message string) {}
func (n *NullIndicator) Update(message string) {}
func (n *NullIndicator) Complete(message string) {}
func (n *NullIndicator) Fail(message string) {}
func (n *NullIndicator) Stop() {}
func (n *NullIndicator) SetEstimator(estimator *ETAEstimator) {}