336 lines
12 KiB
C#
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,
|
|
};
|
|
}
|
|
}
|