Files
Scripts/hanatui/src/Tui/OperationForms.cs
T
2026-05-20 11:26:44 +02:00

336 lines
12 KiB
C#

using HanaTui.Hana;
using Spectre.Console;
namespace HanaTui.Tui;
/// <summary>
/// Guided input forms for each operation.
/// Each method collects all needed parameters, then returns a typed params object.
/// Returns null if the user cancelled.
/// </summary>
public static class OperationForms
{
// -------------------------------------------------------------------------
// Shared helpers
// -------------------------------------------------------------------------
/// <summary>
/// A choice item that keeps the real value separate from the display label.
/// The display label is pre-escaped plain text — never interpreted as markup.
/// </summary>
private sealed class SchemaChoice
{
public string Display { get; init; } = "";
public string? Value { get; init; } // null = cancel, "" = manual entry
public static readonly SchemaChoice Manual = new() { Display = "[ Enter manually ]", Value = "" };
public static readonly SchemaChoice Cancel = new() { Display = "[ Cancel ]", Value = null };
public override string ToString() => Display;
}
/// <summary>
/// Fetches the schema list with a spinner, then shows an arrow-key picker.
/// Returns the selected schema, or null if cancelled.
/// </summary>
private static string? PickSchema(string userKey, string title)
{
List<string> schemas = [];
string? error = null;
AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("blue"))
.Start("[blue]Fetching schemas...[/]", _ =>
{
var (success, list, err) = HdbCliRunner.ListSchemas(userKey);
if (success)
schemas = list;
else
error = err;
});
if (error is not null)
AnsiConsole.MarkupLine($"[yellow][[WARN]] Could not fetch schemas: {Markup.Escape(error)}[/]");
// Build typed choices. Display is plain text; ToString() is what SelectionPrompt shows.
var choices = schemas
.Select(s => new SchemaChoice { Display = s, Value = s })
.ToList();
choices.Add(SchemaChoice.Manual);
choices.Add(SchemaChoice.Cancel);
// UseConverter returns Display which is already plain text.
// We also set the prompt to NOT interpret converter output as markup by
// escaping it — belt-and-suspenders.
var prompt = new SelectionPrompt<SchemaChoice>()
.Title($"[bold]{Markup.Escape(title)}[/]")
.PageSize(15)
.HighlightStyle(Style.Parse("bold dodgerblue1"))
.UseConverter(c => Markup.Escape(c.Display))
.AddChoices(choices);
var selected = AnsiConsole.Prompt(prompt);
if (selected.Value is null)
return null; // Cancel
if (selected.Value == "")
{
var manual = AnsiConsole.Ask<string>("Enter schema name:").Trim();
return string.IsNullOrWhiteSpace(manual) ? null : manual;
}
return selected.Value;
}
private static int PickThreads(string label = "Number of threads")
{
var maxThreads = Environment.ProcessorCount;
var defaultThreads = Math.Max(1, maxThreads / 2);
return AnsiConsole.Ask($"{label} [dim](default={defaultThreads}, max={maxThreads})[/]:",
defaultThreads);
}
private static bool ConfirmYesNo(string question, bool defaultYes = false)
{
var defaultLabel = defaultYes ? "Y/n" : "y/N";
var answer = AnsiConsole.Ask<string>($"{question} [dim]({defaultLabel})[/]:", defaultYes ? "y" : "n");
return answer.Trim().Equals("y", StringComparison.OrdinalIgnoreCase) ||
answer.Trim().Equals("yes", StringComparison.OrdinalIgnoreCase);
}
private static void PrintOperationHeader(string title)
{
AnsiConsole.Clear();
var rule = new Rule($"[bold dodgerblue1]{Markup.Escape(title)}[/]").RuleStyle(Style.Parse("dodgerblue1"));
AnsiConsole.Write(rule);
AnsiConsole.WriteLine();
}
private static bool ShowSummaryAndConfirm(string title, Dictionary<string, string> fields)
{
AnsiConsole.WriteLine();
var table = new Table().BorderColor(Color.DodgerBlue1).Border(TableBorder.Rounded);
table.AddColumn("[bold]Parameter[/]");
table.AddColumn("[bold]Value[/]");
foreach (var (k, v) in fields)
table.AddRow($"[dim]{Markup.Escape(k)}[/]", $"[yellow]{Markup.Escape(v)}[/]");
AnsiConsole.MarkupLine($"[bold]{Markup.Escape(title)}[/]");
AnsiConsole.Write(table);
AnsiConsole.WriteLine();
return ConfirmYesNo("Proceed with this operation?", defaultYes: true);
}
// -------------------------------------------------------------------------
// Export
// -------------------------------------------------------------------------
public static ExportParams? ExportForm(string userKey)
{
PrintOperationHeader("Export Schema");
var schema = PickSchema(userKey, "Select schema to export:");
if (schema is null) return null;
var targetPath = AnsiConsole.Ask<string>("Target directory or file path [dim](.tar.gz for archive)[/]:").Trim();
if (string.IsNullOrWhiteSpace(targetPath)) return null;
var threads = PickThreads();
var compress = ConfirmYesNo("Compress output as .tar.gz?");
var confirmed = ShowSummaryAndConfirm("Export Summary", new Dictionary<string, string>
{
["Schema"] = schema,
["Target path"] = targetPath,
["Threads"] = threads.ToString(),
["Compress"] = compress ? "Yes" : "No",
});
if (!confirmed) return null;
return new ExportParams
{
Schema = schema,
TargetPath = targetPath,
Threads = threads,
Compress = compress,
};
}
// -------------------------------------------------------------------------
// Import
// -------------------------------------------------------------------------
public static ImportParams? ImportForm(string userKey, bool renameMode)
{
var title = renameMode ? "Import & Rename Schema" : "Import Schema";
PrintOperationHeader(title);
var sourceSchema = AnsiConsole.Ask<string>("Source schema name [dim](as it was exported)[/]:").Trim();
if (string.IsNullOrWhiteSpace(sourceSchema)) return null;
string? newSchemaName = null;
if (renameMode)
{
newSchemaName = AnsiConsole.Ask<string>("New target schema name:").Trim();
if (string.IsNullOrWhiteSpace(newSchemaName)) return null;
}
var sourcePath = AnsiConsole.Ask<string>("Source path [dim](directory or .tar.gz)[/]:").Trim();
if (string.IsNullOrWhiteSpace(sourcePath)) return null;
var threads = PickThreads();
var replace = ConfirmYesNo("Replace existing objects?");
var summary = new Dictionary<string, string>
{
["Source schema"] = sourceSchema,
["Source path"] = sourcePath,
["Threads"] = threads.ToString(),
["Replace"] = replace ? "Yes" : "No (IGNORE EXISTING)",
};
if (renameMode && newSchemaName is not null)
summary["New schema name"] = newSchemaName;
var confirmed = ShowSummaryAndConfirm($"{title} Summary", summary);
if (!confirmed) return null;
return new ImportParams
{
SourceSchema = sourceSchema,
NewSchemaName = newSchemaName,
SourcePath = sourcePath,
Threads = threads,
Replace = replace,
};
}
// -------------------------------------------------------------------------
// Copy
// -------------------------------------------------------------------------
public static CopyParams? CopyForm(string userKey)
{
PrintOperationHeader("Copy Schema");
var sourceSchema = PickSchema(userKey, "Select source schema:");
if (sourceSchema is null) return null;
var targetSchema = AnsiConsole.Ask<string>("Target schema name:").Trim();
if (string.IsNullOrWhiteSpace(targetSchema)) return null;
var tempPath = AnsiConsole.Ask<string>("Temporary export directory path:").Trim();
if (string.IsNullOrWhiteSpace(tempPath)) return null;
var threads = PickThreads();
var replace = ConfirmYesNo("Replace existing objects in target schema?");
var confirmed = ShowSummaryAndConfirm("Copy Schema Summary", new Dictionary<string, string>
{
["Source schema"] = sourceSchema,
["Target schema"] = targetSchema,
["Temp path"] = tempPath,
["Threads"] = threads.ToString(),
["Replace"] = replace ? "Yes" : "No",
});
if (!confirmed) return null;
return new CopyParams
{
SourceSchema = sourceSchema,
TargetSchema = targetSchema,
TempPath = tempPath,
Threads = threads,
Replace = replace,
};
}
// -------------------------------------------------------------------------
// Drop
// -------------------------------------------------------------------------
public static DropParams? DropForm(string userKey)
{
PrintOperationHeader("Drop Schema");
var schema = PickSchema(userKey, "Select schema to drop:");
if (schema is null) return null;
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine($"[bold red]WARNING:[/] You are about to [bold red]permanently drop[/] schema [bold yellow]{Markup.Escape(schema)}[/].");
AnsiConsole.MarkupLine("[red]This cannot be undone.[/]\n");
var confirm = AnsiConsole.Ask<string>($"Type [bold red]YES[/] to confirm dropping [yellow]{Markup.Escape(schema)}[/]:").Trim();
if (confirm != "YES")
{
AnsiConsole.MarkupLine("[yellow]Operation cancelled.[/]");
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[dim]Press any key to return...[/]");
Console.ReadKey(intercept: true);
return null;
}
return new DropParams { Schema = schema };
}
// -------------------------------------------------------------------------
// Rename DB
// -------------------------------------------------------------------------
public static RenameDbParams? RenameDbForm(string userKey)
{
PrintOperationHeader("Rename Database (Company Name)");
var schema = PickSchema(userKey, "Select schema:");
if (schema is null) return null;
var newName = AnsiConsole.Ask<string>("New company name:").Trim();
if (string.IsNullOrWhiteSpace(newName)) return null;
var confirmed = ShowSummaryAndConfirm("Rename DB Summary", new Dictionary<string, string>
{
["Schema"] = schema,
["New company name"] = newName,
["Tables updated"] = "CINF, OADM",
});
if (!confirmed) return null;
return new RenameDbParams { Schema = schema, NewCompanyName = newName };
}
// -------------------------------------------------------------------------
// Backup
// -------------------------------------------------------------------------
public static BackupParams? BackupForm(string userKey)
{
PrintOperationHeader("Backup Tenant");
var targetPath = AnsiConsole.Ask<string>("Target directory path:").Trim();
if (string.IsNullOrWhiteSpace(targetPath)) return null;
var threads = PickThreads("Number of compression threads");
var compress = ConfirmYesNo("Compress backup as .tar.gz?");
var confirmed = ShowSummaryAndConfirm("Backup Summary", new Dictionary<string, string>
{
["Target path"] = targetPath,
["Threads"] = threads.ToString(),
["Compress"] = compress ? "Yes" : "No",
});
if (!confirmed) return null;
return new BackupParams
{
TargetPath = targetPath,
Threads = threads,
Compress = compress,
};
}
}