Add Option C: Smart cluster cleanup before restore (TUI)
- Auto-detects existing user databases before cluster restore - Shows count and list (first 5) in preview screen - Toggle option 'c' to enable cluster cleanup - Drops all user databases before restore when enabled - Works for PostgreSQL, MySQL, MariaDB - Safety warning with database count - Implements practical disaster recovery workflow
This commit is contained in:
@@ -337,3 +337,85 @@ func (s *Safety) checkMySQLDatabaseExists(ctx context.Context, dbName string) (b
|
|||||||
|
|
||||||
return strings.Contains(string(output), dbName), nil
|
return strings.Contains(string(output), dbName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListUserDatabases returns list of user databases (excludes templates and system DBs)
|
||||||
|
func (s *Safety) ListUserDatabases(ctx context.Context) ([]string, error) {
|
||||||
|
if s.cfg.DatabaseType == "postgres" {
|
||||||
|
return s.listPostgresUserDatabases(ctx)
|
||||||
|
} else if s.cfg.DatabaseType == "mysql" || s.cfg.DatabaseType == "mariadb" {
|
||||||
|
return s.listMySQLUserDatabases(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unsupported database type: %s", s.cfg.DatabaseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listPostgresUserDatabases lists PostgreSQL user databases
|
||||||
|
func (s *Safety) listPostgresUserDatabases(ctx context.Context) ([]string, error) {
|
||||||
|
// Query to get non-template databases excluding 'postgres' system DB
|
||||||
|
query := "SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres' ORDER BY datname"
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx,
|
||||||
|
"psql",
|
||||||
|
"-h", s.cfg.Host,
|
||||||
|
"-p", fmt.Sprintf("%d", s.cfg.Port),
|
||||||
|
"-U", s.cfg.User,
|
||||||
|
"-d", "postgres",
|
||||||
|
"-tA", // Tuples only, unaligned
|
||||||
|
"-c", query,
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", s.cfg.Password))
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list databases: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse output
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
databases := []string{}
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
databases = append(databases, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return databases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// listMySQLUserDatabases lists MySQL/MariaDB user databases
|
||||||
|
func (s *Safety) listMySQLUserDatabases(ctx context.Context) ([]string, error) {
|
||||||
|
// Exclude system databases
|
||||||
|
query := "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys') ORDER BY SCHEMA_NAME"
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx,
|
||||||
|
"mysql",
|
||||||
|
"-h", s.cfg.Host,
|
||||||
|
"-P", fmt.Sprintf("%d", s.cfg.Port),
|
||||||
|
"-u", s.cfg.User,
|
||||||
|
"-N", // Skip column names
|
||||||
|
"-e", query,
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.cfg.Password != "" {
|
||||||
|
cmd.Env = append(os.Environ(), fmt.Sprintf("MYSQL_PWD=%s", s.cfg.Password))
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list databases: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse output
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
databases := []string{}
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
databases = append(databases, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return databases, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,14 +16,16 @@ import (
|
|||||||
|
|
||||||
// RestoreExecutionModel handles restore execution with progress
|
// RestoreExecutionModel handles restore execution with progress
|
||||||
type RestoreExecutionModel struct {
|
type RestoreExecutionModel struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
archive ArchiveInfo
|
archive ArchiveInfo
|
||||||
targetDB string
|
targetDB string
|
||||||
cleanFirst bool
|
cleanFirst bool
|
||||||
createIfMissing bool
|
createIfMissing bool
|
||||||
restoreType string
|
restoreType string
|
||||||
|
cleanClusterFirst bool // Drop all user databases before cluster restore
|
||||||
|
existingDBs []string // List of databases to drop
|
||||||
|
|
||||||
// Progress tracking
|
// Progress tracking
|
||||||
status string
|
status string
|
||||||
@@ -42,28 +44,30 @@ type RestoreExecutionModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRestoreExecution creates a new restore execution model
|
// NewRestoreExecution creates a new restore execution model
|
||||||
func NewRestoreExecution(cfg *config.Config, log logger.Logger, parent tea.Model, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string) RestoreExecutionModel {
|
func NewRestoreExecution(cfg *config.Config, log logger.Logger, parent tea.Model, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string, cleanClusterFirst bool, existingDBs []string) RestoreExecutionModel {
|
||||||
return RestoreExecutionModel{
|
return RestoreExecutionModel{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: log,
|
logger: log,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
archive: archive,
|
archive: archive,
|
||||||
targetDB: targetDB,
|
targetDB: targetDB,
|
||||||
cleanFirst: cleanFirst,
|
cleanFirst: cleanFirst,
|
||||||
createIfMissing: createIfMissing,
|
createIfMissing: createIfMissing,
|
||||||
restoreType: restoreType,
|
restoreType: restoreType,
|
||||||
status: "Initializing...",
|
cleanClusterFirst: cleanClusterFirst,
|
||||||
phase: "Starting",
|
existingDBs: existingDBs,
|
||||||
startTime: time.Now(),
|
status: "Initializing...",
|
||||||
details: []string{},
|
phase: "Starting",
|
||||||
spinnerFrames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
|
startTime: time.Now(),
|
||||||
spinnerFrame: 0,
|
details: []string{},
|
||||||
|
spinnerFrames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
|
||||||
|
spinnerFrame: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m RestoreExecutionModel) Init() tea.Cmd {
|
func (m RestoreExecutionModel) Init() tea.Cmd {
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
executeRestoreWithTUIProgress(m.config, m.logger, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.restoreType),
|
executeRestoreWithTUIProgress(m.config, m.logger, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.restoreType, m.cleanClusterFirst, m.existingDBs),
|
||||||
restoreTickCmd(),
|
restoreTickCmd(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -89,7 +93,7 @@ type restoreCompleteMsg struct {
|
|||||||
elapsed time.Duration
|
elapsed time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeRestoreWithTUIProgress(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string) tea.Cmd {
|
func executeRestoreWithTUIProgress(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string, cleanClusterFirst bool, existingDBs []string) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -107,13 +111,41 @@ func executeRestoreWithTUIProgress(cfg *config.Config, log logger.Logger, archiv
|
|||||||
}
|
}
|
||||||
defer dbClient.Close()
|
defer dbClient.Close()
|
||||||
|
|
||||||
// Create restore engine with silent progress (no stdout interference with TUI)
|
// STEP 1: Clean cluster if requested (drop all existing user databases)
|
||||||
|
if restoreType == "restore-cluster" && cleanClusterFirst && len(existingDBs) > 0 {
|
||||||
|
log.Info("Dropping existing user databases before cluster restore", "count", len(existingDBs))
|
||||||
|
|
||||||
|
// Connect to database for cleanup
|
||||||
|
if err := dbClient.Connect(ctx); err != nil {
|
||||||
|
return restoreCompleteMsg{
|
||||||
|
result: "",
|
||||||
|
err: fmt.Errorf("failed to connect for cleanup: %w", err),
|
||||||
|
elapsed: time.Since(start),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop each database
|
||||||
|
droppedCount := 0
|
||||||
|
for _, dbName := range existingDBs {
|
||||||
|
if err := dbClient.DropDatabase(ctx, dbName); err != nil {
|
||||||
|
log.Warn("Failed to drop database", "name", dbName, "error", err)
|
||||||
|
// Continue with other databases
|
||||||
|
} else {
|
||||||
|
droppedCount++
|
||||||
|
log.Info("Dropped database", "name", dbName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Cluster cleanup completed", "dropped", droppedCount, "total", len(existingDBs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 2: Create restore engine with silent progress (no stdout interference with TUI)
|
||||||
engine := restore.NewSilent(cfg, log, dbClient)
|
engine := restore.NewSilent(cfg, log, dbClient)
|
||||||
|
|
||||||
// Set up progress callback (but it won't work in goroutine - progress is already sent via logs)
|
// Set up progress callback (but it won't work in goroutine - progress is already sent via logs)
|
||||||
// The TUI will just use spinner animation to show activity
|
// The TUI will just use spinner animation to show activity
|
||||||
|
|
||||||
// Execute restore based on type
|
// STEP 3: Execute restore based on type
|
||||||
var restoreErr error
|
var restoreErr error
|
||||||
if restoreType == "restore-cluster" {
|
if restoreType == "restore-cluster" {
|
||||||
restoreErr = engine.RestoreCluster(ctx, archive.Path)
|
restoreErr = engine.RestoreCluster(ctx, archive.Path)
|
||||||
@@ -132,6 +164,8 @@ func executeRestoreWithTUIProgress(cfg *config.Config, log logger.Logger, archiv
|
|||||||
result := fmt.Sprintf("Successfully restored from %s", archive.Name)
|
result := fmt.Sprintf("Successfully restored from %s", archive.Name)
|
||||||
if restoreType == "restore-single" {
|
if restoreType == "restore-single" {
|
||||||
result = fmt.Sprintf("Successfully restored '%s' from %s", targetDB, archive.Name)
|
result = fmt.Sprintf("Successfully restored '%s' from %s", targetDB, archive.Name)
|
||||||
|
} else if restoreType == "restore-cluster" && cleanClusterFirst {
|
||||||
|
result = fmt.Sprintf("Successfully restored cluster from %s (cleaned %d existing database(s) first)", archive.Name, len(existingDBs))
|
||||||
}
|
}
|
||||||
|
|
||||||
return restoreCompleteMsg{
|
return restoreCompleteMsg{
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ type RestorePreviewModel struct {
|
|||||||
targetDB string
|
targetDB string
|
||||||
cleanFirst bool
|
cleanFirst bool
|
||||||
createIfMissing bool
|
createIfMissing bool
|
||||||
|
cleanClusterFirst bool // For cluster restore: drop all user databases first
|
||||||
|
existingDBCount int // Number of existing user databases
|
||||||
|
existingDBs []string // List of existing user databases
|
||||||
safetyChecks []SafetyCheck
|
safetyChecks []SafetyCheck
|
||||||
checking bool
|
checking bool
|
||||||
canProceed bool
|
canProceed bool
|
||||||
@@ -89,8 +92,10 @@ func (m RestorePreviewModel) Init() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type safetyCheckCompleteMsg struct {
|
type safetyCheckCompleteMsg struct {
|
||||||
checks []SafetyCheck
|
checks []SafetyCheck
|
||||||
canProceed bool
|
canProceed bool
|
||||||
|
existingDBCount int
|
||||||
|
existingDBs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string) tea.Cmd {
|
func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string) tea.Cmd {
|
||||||
@@ -147,6 +152,9 @@ func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo,
|
|||||||
checks = append(checks, check)
|
checks = append(checks, check)
|
||||||
|
|
||||||
// 4. Target database check (skip for cluster restores)
|
// 4. Target database check (skip for cluster restores)
|
||||||
|
existingDBCount := 0
|
||||||
|
existingDBs := []string{}
|
||||||
|
|
||||||
if !archive.Format.IsClusterBackup() {
|
if !archive.Format.IsClusterBackup() {
|
||||||
check = SafetyCheck{Name: "Target database", Status: "checking", Critical: false}
|
check = SafetyCheck{Name: "Target database", Status: "checking", Critical: false}
|
||||||
exists, err := safety.CheckDatabaseExists(ctx, targetDB)
|
exists, err := safety.CheckDatabaseExists(ctx, targetDB)
|
||||||
@@ -162,13 +170,35 @@ func runSafetyChecks(cfg *config.Config, log logger.Logger, archive ArchiveInfo,
|
|||||||
}
|
}
|
||||||
checks = append(checks, check)
|
checks = append(checks, check)
|
||||||
} else {
|
} else {
|
||||||
// For cluster restores, just show a general message
|
// For cluster restores, detect existing user databases
|
||||||
check = SafetyCheck{Name: "Cluster restore", Status: "passed", Critical: false}
|
check = SafetyCheck{Name: "Existing databases", Status: "checking", Critical: false}
|
||||||
check.Message = "Will restore all databases from cluster backup"
|
|
||||||
|
// Get list of existing user databases (exclude templates and system DBs)
|
||||||
|
dbList, err := safety.ListUserDatabases(ctx)
|
||||||
|
if err != nil {
|
||||||
|
check.Status = "warning"
|
||||||
|
check.Message = fmt.Sprintf("Cannot list databases: %v", err)
|
||||||
|
} else {
|
||||||
|
existingDBCount = len(dbList)
|
||||||
|
existingDBs = dbList
|
||||||
|
|
||||||
|
if existingDBCount > 0 {
|
||||||
|
check.Status = "warning"
|
||||||
|
check.Message = fmt.Sprintf("Found %d existing user database(s) - can be cleaned before restore", existingDBCount)
|
||||||
|
} else {
|
||||||
|
check.Status = "passed"
|
||||||
|
check.Message = "No existing user databases - clean slate"
|
||||||
|
}
|
||||||
|
}
|
||||||
checks = append(checks, check)
|
checks = append(checks, check)
|
||||||
}
|
}
|
||||||
|
|
||||||
return safetyCheckCompleteMsg{checks: checks, canProceed: canProceed}
|
return safetyCheckCompleteMsg{
|
||||||
|
checks: checks,
|
||||||
|
canProceed: canProceed,
|
||||||
|
existingDBCount: existingDBCount,
|
||||||
|
existingDBs: existingDBs,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +208,8 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.checking = false
|
m.checking = false
|
||||||
m.safetyChecks = msg.checks
|
m.safetyChecks = msg.checks
|
||||||
m.canProceed = msg.canProceed
|
m.canProceed = msg.canProceed
|
||||||
|
m.existingDBCount = msg.existingDBCount
|
||||||
|
m.existingDBs = msg.existingDBs
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
@@ -191,9 +223,19 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.message = fmt.Sprintf("Clean-first: %v", m.cleanFirst)
|
m.message = fmt.Sprintf("Clean-first: %v", m.cleanFirst)
|
||||||
|
|
||||||
case "c":
|
case "c":
|
||||||
// Toggle create if missing
|
if m.mode == "restore-cluster" {
|
||||||
m.createIfMissing = !m.createIfMissing
|
// Toggle cluster cleanup
|
||||||
m.message = fmt.Sprintf("Create if missing: %v", m.createIfMissing)
|
m.cleanClusterFirst = !m.cleanClusterFirst
|
||||||
|
if m.cleanClusterFirst {
|
||||||
|
m.message = checkWarningStyle.Render(fmt.Sprintf("⚠️ Will drop %d existing database(s) before restore", m.existingDBCount))
|
||||||
|
} else {
|
||||||
|
m.message = fmt.Sprintf("Clean cluster first: disabled")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Toggle create if missing
|
||||||
|
m.createIfMissing = !m.createIfMissing
|
||||||
|
m.message = fmt.Sprintf("Create if missing: %v", m.createIfMissing)
|
||||||
|
}
|
||||||
|
|
||||||
case "enter", " ":
|
case "enter", " ":
|
||||||
if m.checking {
|
if m.checking {
|
||||||
@@ -207,7 +249,7 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Proceed to restore execution
|
// Proceed to restore execution
|
||||||
exec := NewRestoreExecution(m.config, m.logger, m.parent, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.mode)
|
exec := NewRestoreExecution(m.config, m.logger, m.parent, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.mode, m.cleanClusterFirst, m.existingDBs)
|
||||||
return exec, exec.Init()
|
return exec, exec.Init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +280,7 @@ func (m RestorePreviewModel) View() string {
|
|||||||
}
|
}
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
|
|
||||||
// Target Information (only for single restore)
|
// Target Information
|
||||||
if m.mode == "restore-single" {
|
if m.mode == "restore-single" {
|
||||||
s.WriteString(archiveHeaderStyle.Render("🎯 Target Information"))
|
s.WriteString(archiveHeaderStyle.Render("🎯 Target Information"))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
@@ -257,6 +299,36 @@ func (m RestorePreviewModel) View() string {
|
|||||||
}
|
}
|
||||||
s.WriteString(fmt.Sprintf(" Create If Missing: %s %v\n", createIcon, m.createIfMissing))
|
s.WriteString(fmt.Sprintf(" Create If Missing: %s %v\n", createIcon, m.createIfMissing))
|
||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
|
} else if m.mode == "restore-cluster" {
|
||||||
|
s.WriteString(archiveHeaderStyle.Render("🎯 Cluster Restore Options"))
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(fmt.Sprintf(" Host: %s:%d\n", m.config.Host, m.config.Port))
|
||||||
|
|
||||||
|
if m.existingDBCount > 0 {
|
||||||
|
s.WriteString(fmt.Sprintf(" Existing Databases: %d found\n", m.existingDBCount))
|
||||||
|
|
||||||
|
// Show first few database names
|
||||||
|
maxShow := 5
|
||||||
|
for i, db := range m.existingDBs {
|
||||||
|
if i >= maxShow {
|
||||||
|
remaining := len(m.existingDBs) - maxShow
|
||||||
|
s.WriteString(fmt.Sprintf(" ... and %d more\n", remaining))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.WriteString(fmt.Sprintf(" - %s\n", db))
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanIcon := "✗"
|
||||||
|
cleanStyle := infoStyle
|
||||||
|
if m.cleanClusterFirst {
|
||||||
|
cleanIcon = "✓"
|
||||||
|
cleanStyle = checkWarningStyle
|
||||||
|
}
|
||||||
|
s.WriteString(cleanStyle.Render(fmt.Sprintf(" Clean All First: %s %v (press 'c' to toggle)\n", cleanIcon, m.cleanClusterFirst)))
|
||||||
|
} else {
|
||||||
|
s.WriteString(" Existing Databases: None (clean slate)\n")
|
||||||
|
}
|
||||||
|
s.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safety Checks
|
// Safety Checks
|
||||||
@@ -303,6 +375,14 @@ func (m RestorePreviewModel) View() string {
|
|||||||
s.WriteString(infoStyle.Render(" All existing data in target database will be dropped!"))
|
s.WriteString(infoStyle.Render(" All existing data in target database will be dropped!"))
|
||||||
s.WriteString("\n\n")
|
s.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
|
if m.cleanClusterFirst && m.existingDBCount > 0 {
|
||||||
|
s.WriteString(checkWarningStyle.Render("🔥 WARNING: Cluster cleanup enabled"))
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(checkWarningStyle.Render(fmt.Sprintf(" %d existing database(s) will be DROPPED before restore!", m.existingDBCount)))
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(infoStyle.Render(" This ensures a clean disaster recovery scenario"))
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
if m.message != "" {
|
if m.message != "" {
|
||||||
@@ -318,6 +398,12 @@ func (m RestorePreviewModel) View() string {
|
|||||||
s.WriteString("\n")
|
s.WriteString("\n")
|
||||||
if m.mode == "restore-single" {
|
if m.mode == "restore-single" {
|
||||||
s.WriteString(infoStyle.Render("⌨️ t: Toggle clean-first | c: Toggle create | Enter: Proceed | Esc: Cancel"))
|
s.WriteString(infoStyle.Render("⌨️ t: Toggle clean-first | c: Toggle create | Enter: Proceed | Esc: Cancel"))
|
||||||
|
} else if m.mode == "restore-cluster" {
|
||||||
|
if m.existingDBCount > 0 {
|
||||||
|
s.WriteString(infoStyle.Render("⌨️ c: Toggle cleanup | Enter: Proceed | Esc: Cancel"))
|
||||||
|
} else {
|
||||||
|
s.WriteString(infoStyle.Render("⌨️ Enter: Proceed | Esc: Cancel"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
s.WriteString(infoStyle.Render("⌨️ Enter: Proceed | Esc: Cancel"))
|
s.WriteString(infoStyle.Render("⌨️ Enter: Proceed | Esc: Cancel"))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user