CLI w Webie?

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);
    }
}