using HanaTui.Hana; using Spectre.Console; namespace HanaTui.Tui; /// /// Guided input forms for each operation. /// Each method collects all needed parameters, then returns a typed params object. /// Returns null if the user cancelled. /// public static class OperationForms { // ------------------------------------------------------------------------- // Shared helpers // ------------------------------------------------------------------------- /// /// 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. /// 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; } /// /// Fetches the schema list with a spinner, then shows an arrow-key picker. /// Returns the selected schema, or null if cancelled. /// private static string? PickSchema(string userKey, string title) { List 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() .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("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($"{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 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("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 { ["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("Source schema name [dim](as it was exported)[/]:").Trim(); if (string.IsNullOrWhiteSpace(sourceSchema)) return null; string? newSchemaName = null; if (renameMode) { newSchemaName = AnsiConsole.Ask("New target schema name:").Trim(); if (string.IsNullOrWhiteSpace(newSchemaName)) return null; } var sourcePath = AnsiConsole.Ask("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 { ["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("Target schema name:").Trim(); if (string.IsNullOrWhiteSpace(targetSchema)) return null; var tempPath = AnsiConsole.Ask("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 { ["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($"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("New company name:").Trim(); if (string.IsNullOrWhiteSpace(newName)) return null; var confirmed = ShowSummaryAndConfirm("Rename DB Summary", new Dictionary { ["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("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 { ["Target path"] = targetPath, ["Threads"] = threads.ToString(), ["Compress"] = compress ? "Yes" : "No", }); if (!confirmed) return null; return new BackupParams { TargetPath = targetPath, Threads = threads, Compress = compress, }; } }