Jakiś czas temu w poprzednim wpisie poruszałem zagadnienie interfejsu linii komend (z ang. Command Line Interface, w skrócie CLI), podając przykład realizacji wykorzystującej (z dużym powodzeniem) taki właśnie model dostępu do logiki systemu.
Pisząc wspomniany tekst naszła mnie myśl - a gdyby przenieść takie rozwiązanie do przeglądarki? Mogłoby to rozwiązać liczne wady CLI, związane z koniecznością dostępu do powłoki / pulpitu OS.
Postanowiłem więc poświęcić kilka sobotnich wieczorów i przygotować proof-of-concept. Jako stos technologiczny wybrałem:
Jako technologię back-endu, z którą mam najwięcej osobistych doświadczeń - ASP.NET Core i .NET Core 9 po stronie back-endu,
Jako technologię front-endu, której znajomość aktualnie pogłębiam - React.JS oraz React Bootstrap.
Taki miks technologiczny pozwolił mi szybko prototypować, a i w razie niepowodzenia zapewnić mi pewną dozę nauki i doświadczeń mogących zaprocentować w przyszłości.
Eksperyment okazał się bardzo pouczający, a rezultaty dość obiecujące. Udało mi się bowiem w tak krótkim czasie stworzyć proste narzędzie imitujące prostotę, lekkość i możliwości szybkiej implementacji nowych funkcjonalności, które tak ceniłem w CLI (zwłaszcza wspartego przez biblioteki typu Spectre.Console).
Rozwiązanie umożliwia łatwą integrację z istniejącymi aplikacjami opartymi o ASP.NET Core (z drobnymi zabiegami udało mi się nawet zintegrować z Umbraco CMS w wersji 16), mogący być dystrybuowane w formie pakietu NuGet. Przy tym oferuje możliwość:
Automatycznego generowania menu z dostępnymi opcjami (komendami) - wspierając realizację zasady Open-Closed-Principle,
Pobieranie danych wejściowych od Użytkownika w formie pól tekstowych, pola liczbowego, plików, opcji drop-down/select,
Zwracania odpowiedzi w formie tekstu, alertów, komunikatów, plików, tabel.
Poniżej prezentuję nagranie obrazujące rezultat wywołania przykładowej komendy oraz jej kod źródłowy.
Prawdopodobnie temat doczeka się kontynuacji, być może nawet publikacji w formie biblioteki open-source. Wszystko w swoim czasie.
[WebCommand("Eksportuj użytkowników", "Eksportuje użytkowników w formie pliku CSV")]
public class TestCommand : WebInteractiveCommand
{
public TestCommand(IWebConsole console)
: base(console)
{
}
public override async Task ExecuteAsync()
{
do
{
await Console.OutputAlert("Uwaga, działasz na uprawnieniach administratora!", AlertVariants.Warning);
var fileName = await Console.PromptText("Aby móc zapisać dane musimy poznać nazwę pliku, który chcesz utworzyć.", "Nazwa pliku", false);
var fileDescription = await Console.PromptText("A teraz proszę opisz krótko wgrywany plik", "Skrócony opis pliku", true);
var uploadedFile = await Console.PromptFileUploadAsync("To teraz Ty coś wyślij...", ".csv");
using (var reader = new StreamReader(await Console.OpenTempFileForReadAsync(uploadedFile)))
{
string content = await reader.ReadToEndAsync();
var lines = content.Count(c => c == '\n');
await Console.OutputAlert($"Sukces. Wgrany plik zawiera {lines} linijek danych", AlertVariants.Success);
}
await Console.OutTextAsync("Cześć, tutaj ja", "A tutaj jest druga linijka tekstu...", "I co Ty na to?");
await Console.OutTextAsync("A to kolejny tekst zaraz po poprzednim");
await Task.Delay(1000);
var repeatCount = await Console.PromptIntAsync("Jak długą listę chciałbyś zobaczyć", allowNegative: false);
await Task.Delay(1000);
var options = Enumerable.Range(0, repeatCount).Select(x => new SelectOption(x.ToString(), $"Opcja nr {x}")).ToArray();
var option = await Console.PromptSelect(options);
await Console.OutTextAsync("No to teraz czas na tabelę....");
await Task.Delay(1000);
var table = new Table()
.SetTitle("Testowa tabelka")
.AddColumn("#")
.AddColumn("Kod")
.AddColumn("Nazwa")
.AddColumn("Adres");
for (int i = 0; i < 10; i++)
{
table.AddRow($"{i + 1}", $"CE{i + 1}", $"{i + 1}01 S.A", "ul. Bajkowa 52, 85-333 Bydgoszcz");
}
await Console.OutTableAsync(table);
await Console.OutTextAsync("To teraz jakiś plik...");
await Task.Delay(500);
var file = await Console.CreateTempFileAsync("test.csv");
using (var fileStream = await Console.OpenTempFileForWriteAsync(file))
using (var writer = new StreamWriter(fileStream))
{
writer.WriteLine("Kolumna A;Kolumna B;Kolumna C");
for (int i = 0; i < 10000; i++)
writer.WriteLine($"{Random.Shared.Next()};{Random.Shared.Next()};{Random.Shared.Next()}");
}
await Console.OutFileAsync(file);
var quitOption = await Console.PromptSelect("Kończymy?", "Odpowiedź", new SelectOption("true", "Tak"), new SelectOption("false", "Nie"));
if (quitOption?.Key == "true")
return;
}
while (true);
}
}