|  | 
|  | 1 | +@attribute [ExcludeFromCodeCoverage] | 
|  | 2 | +@page "/rules-ui/rql-terminal" | 
|  | 3 | +@using Rules.Framework.Rql | 
|  | 4 | +@using Rules.Framework.Rql.Runtime.Types | 
|  | 5 | +@using System.Text | 
|  | 6 | +@using System.Text.Json | 
|  | 7 | +@rendermode InteractiveServer | 
|  | 8 | +@inject IJSRuntime JS | 
|  | 9 | +@inject WebUIOptions Options | 
|  | 10 | +@inject IRulesEngineInstanceProvider RulesEngineInstanceProvider | 
|  | 11 | +@inject ProtectedSessionStorage Storage | 
|  | 12 | + | 
|  | 13 | +<PageTitle>@(this.Options.DocumentTitle) - RQL Terminal</PageTitle> | 
|  | 14 | + | 
|  | 15 | +<h2>RQL Terminal</h2> | 
|  | 16 | + | 
|  | 17 | +<div class="w-100" onclick="window.focusOnElement('commandInputTextbox')"> | 
|  | 18 | +    <div class="terminal p-2 bg-dark rounded shadow-sm d-flex flex-column-reverse overflow-auto"> | 
|  | 19 | +        <div class="terminal-text text-bg-dark d-flex"> | 
|  | 20 | +            <span id="commandInputLabel" class="pe-2">></span> | 
|  | 21 | +            <input id="commandInputTextbox" | 
|  | 22 | +                type="text" | 
|  | 23 | +                class="terminal-input bg-dark text-bg-dark border-0 flex-fill" | 
|  | 24 | +                aria-describedby="commandInputLabel" | 
|  | 25 | +                @bind-value="commandInput" | 
|  | 26 | +                @onkeyup="OnCommandInputKeyUpAsync" /> | 
|  | 27 | +        </div> | 
|  | 28 | +        <pre class="terminal-output terminal-text text-bg-dark" onclick="window.focusOnElement('commandInputTextbox')"> | 
|  | 29 | +            @foreach (var line in this.outputQueue) | 
|  | 30 | +            { | 
|  | 31 | +                if (!string.IsNullOrWhiteSpace(line)) | 
|  | 32 | +                { | 
|  | 33 | +                    <code class="terminal-line mb-1" onclick="window.focusOnElement('commandInputTextbox')">@((MarkupString)line)</code> | 
|  | 34 | +                } | 
|  | 35 | +                <br class="mb-1" onclick="window.focusOnElement('commandInputTextbox')" /> | 
|  | 36 | +            } | 
|  | 37 | +        </pre> | 
|  | 38 | +    </div> | 
|  | 39 | +</div> | 
|  | 40 | + | 
|  | 41 | +@code { | 
|  | 42 | +    private static readonly string tab = new string(' ', 4); | 
|  | 43 | +    private string commandInput; | 
|  | 44 | +    private LinkedList<string> commandInputHistory; | 
|  | 45 | +    private int commandInputHistoryCount; | 
|  | 46 | +    private LinkedListNode<string> commandInputHistoryCurrent; | 
|  | 47 | +    private Queue<string> outputQueue; | 
|  | 48 | +    private IRqlEngine rqlEngine; | 
|  | 49 | + | 
|  | 50 | +    protected override void OnInitialized() | 
|  | 51 | +    { | 
|  | 52 | +        this.outputQueue = new Queue<string>(this.Options.RqlTerminal.MaxOutputLines); | 
|  | 53 | +        this.commandInputHistory = new LinkedList<string>(); | 
|  | 54 | +        this.commandInputHistoryCount = 0; | 
|  | 55 | +    } | 
|  | 56 | + | 
|  | 57 | +    protected override async Task OnAfterRenderAsync(bool firstRender) | 
|  | 58 | +    { | 
|  | 59 | +        if (firstRender) | 
|  | 60 | +        { | 
|  | 61 | +            var instanceIdResult = await this.Storage.GetAsync<Guid>(WebUIConstants.SelectedInstanceStorageKey); | 
|  | 62 | +            if (instanceIdResult.Success) | 
|  | 63 | +            { | 
|  | 64 | +                var instanceId = instanceIdResult.Value; | 
|  | 65 | +                var rulesEngineInstance = this.RulesEngineInstanceProvider.GetInstance(instanceId); | 
|  | 66 | +                this.rqlEngine = rulesEngineInstance.RulesEngine.GetRqlEngine(); | 
|  | 67 | +                this.StateHasChanged(); | 
|  | 68 | +            } | 
|  | 69 | +        } | 
|  | 70 | + | 
|  | 71 | +        await this.JS.InvokeVoidAsync("scrollToTop", ".terminal > pre"); | 
|  | 72 | +    } | 
|  | 73 | + | 
|  | 74 | +    private async Task OnCommandInputKeyUpAsync(KeyboardEventArgs args) | 
|  | 75 | +    { | 
|  | 76 | +        switch (args.Key) | 
|  | 77 | +        { | 
|  | 78 | +            case "Enter": | 
|  | 79 | +            case "NumpadEnter": | 
|  | 80 | +                if (!string.IsNullOrWhiteSpace(this.commandInput)) | 
|  | 81 | +                { | 
|  | 82 | +                    await ExecuteAsync(this.rqlEngine, this.commandInput); | 
|  | 83 | + | 
|  | 84 | +                    if (this.commandInputHistoryCount >= 50) | 
|  | 85 | +                    { | 
|  | 86 | +                        this.commandInputHistory.RemoveLast(); | 
|  | 87 | +                    } | 
|  | 88 | + | 
|  | 89 | +                    this.commandInputHistory.AddFirst(this.commandInput); | 
|  | 90 | +                    this.commandInput = string.Empty; | 
|  | 91 | +                    this.commandInputHistoryCurrent = null; | 
|  | 92 | +                    this.StateHasChanged(); | 
|  | 93 | +                } | 
|  | 94 | +                break; | 
|  | 95 | + | 
|  | 96 | +            case "ArrowUp": | 
|  | 97 | +                if (this.commandInputHistoryCurrent is not null) | 
|  | 98 | +                { | 
|  | 99 | +                    if (this.commandInputHistoryCurrent.Next is not null) | 
|  | 100 | +                    { | 
|  | 101 | +                        this.commandInputHistoryCurrent = this.commandInputHistoryCurrent.Next; | 
|  | 102 | +                        this.commandInput = this.commandInputHistoryCurrent.Value; | 
|  | 103 | +                    } | 
|  | 104 | +                } | 
|  | 105 | +                else | 
|  | 106 | +                { | 
|  | 107 | +                    this.commandInputHistoryCurrent = this.commandInputHistory.First; | 
|  | 108 | +                    if (this.commandInputHistoryCurrent is not null) | 
|  | 109 | +                    { | 
|  | 110 | +                        this.commandInput = this.commandInputHistoryCurrent.Value; | 
|  | 111 | +                    } | 
|  | 112 | +                } | 
|  | 113 | +                break; | 
|  | 114 | + | 
|  | 115 | +            case "ArrowDown": | 
|  | 116 | +                if (this.commandInputHistoryCurrent is not null) | 
|  | 117 | +                { | 
|  | 118 | +                    if (this.commandInputHistoryCurrent.Previous is not null) | 
|  | 119 | +                    { | 
|  | 120 | +                        this.commandInputHistoryCurrent = this.commandInputHistoryCurrent.Previous; | 
|  | 121 | +                        this.commandInput = this.commandInputHistoryCurrent.Value; | 
|  | 122 | +                    } | 
|  | 123 | +                    else | 
|  | 124 | +                    { | 
|  | 125 | +                        this.commandInput = string.Empty; | 
|  | 126 | +                    } | 
|  | 127 | +                } | 
|  | 128 | +                break; | 
|  | 129 | + | 
|  | 130 | +            default: | 
|  | 131 | +                break; | 
|  | 132 | +        } | 
|  | 133 | +    } | 
|  | 134 | + | 
|  | 135 | +    private async Task ExecuteAsync(IRqlEngine rqlEngine, string? input) | 
|  | 136 | +    { | 
|  | 137 | +        try | 
|  | 138 | +        { | 
|  | 139 | +            WriteLine($"> {input}"); | 
|  | 140 | +            var results = await rqlEngine.ExecuteAsync(input); | 
|  | 141 | +            foreach (var result in results) | 
|  | 142 | +            { | 
|  | 143 | +                switch (result) | 
|  | 144 | +                { | 
|  | 145 | +                    case RulesSetResult rulesResultSet: | 
|  | 146 | +                        HandleRulesSetResult(rulesResultSet); | 
|  | 147 | +                        break; | 
|  | 148 | + | 
|  | 149 | +                    case NothingResult: | 
|  | 150 | +                        // Nothing to be done. | 
|  | 151 | +                        break; | 
|  | 152 | + | 
|  | 153 | +                    case ValueResult valueResult: | 
|  | 154 | +                        HandleObjectResult(valueResult); | 
|  | 155 | +                        break; | 
|  | 156 | + | 
|  | 157 | +                    default: | 
|  | 158 | +                        throw new NotSupportedException($"Result type is not supported: '{result.GetType().FullName}'"); | 
|  | 159 | +                } | 
|  | 160 | +            } | 
|  | 161 | +        } | 
|  | 162 | +        catch (RqlException rqlException) | 
|  | 163 | +        { | 
|  | 164 | +            WriteLine($"{rqlException.Message} Errors:"); | 
|  | 165 | + | 
|  | 166 | +            foreach (var rqlError in rqlException.Errors) | 
|  | 167 | +            { | 
|  | 168 | +                var errorMessageBuilder = new StringBuilder(" - ") | 
|  | 169 | +                    .Append(rqlError.Text) | 
|  | 170 | +                    .Append(" @") | 
|  | 171 | +                    .Append(rqlError.BeginPosition) | 
|  | 172 | +                    .Append('-') | 
|  | 173 | +                    .Append(rqlError.EndPosition); | 
|  | 174 | +                WriteLine(errorMessageBuilder.ToString()); | 
|  | 175 | +            } | 
|  | 176 | +        } | 
|  | 177 | + | 
|  | 178 | +        WriteLine(); | 
|  | 179 | +    } | 
|  | 180 | + | 
|  | 181 | +    private void HandleObjectResult(ValueResult result) | 
|  | 182 | +    { | 
|  | 183 | +        WriteLine(); | 
|  | 184 | +        var rawValue = result.Value switch | 
|  | 185 | +        { | 
|  | 186 | +            RqlAny rqlAny when rqlAny.UnderlyingType == RqlTypes.Object => rqlAny.ToString() ?? string.Empty, | 
|  | 187 | +            RqlAny rqlAny => rqlAny.ToString() ?? string.Empty, | 
|  | 188 | +            _ => result.Value.ToString(), | 
|  | 189 | +        }; | 
|  | 190 | +        var value = rawValue!.Replace("\n", $"\n{tab}"); | 
|  | 191 | +        WriteLine($"{tab}{value}"); | 
|  | 192 | +    } | 
|  | 193 | + | 
|  | 194 | +    private void HandleRulesSetResult(RulesSetResult result) | 
|  | 195 | +    { | 
|  | 196 | +        WriteLine(); | 
|  | 197 | +        if (result.Lines.Any()) | 
|  | 198 | +        { | 
|  | 199 | +            WriteLine($"{tab}{result.Rql}"); | 
|  | 200 | +            WriteLine($"{tab}{new string('-', Math.Min(result.Rql.Length, 20))}"); | 
|  | 201 | +            if (result.NumberOfRules > 0) | 
|  | 202 | +            { | 
|  | 203 | +                WriteLine($"{tab} {result.NumberOfRules} rules were returned."); | 
|  | 204 | +            } | 
|  | 205 | +            else | 
|  | 206 | +            { | 
|  | 207 | +                WriteLine($"{tab} {result.Lines.Count} rules were returned."); | 
|  | 208 | +            } | 
|  | 209 | + | 
|  | 210 | +            WriteLine(); | 
|  | 211 | +            WriteLine($"{tab} | # | Priority | Status   | Range                     | Rule"); | 
|  | 212 | +            WriteLine($"{tab}{new string('-', 20)}"); | 
|  | 213 | + | 
|  | 214 | +            foreach (var line in result.Lines) | 
|  | 215 | +            { | 
|  | 216 | +                var rule = line.Rule.Value; | 
|  | 217 | +                var lineNumber = line.LineNumber.ToString(); | 
|  | 218 | +                var priority = rule.Priority.ToString(); | 
|  | 219 | +                var active = rule.Active ? "Active" : "Inactive"; | 
|  | 220 | +                var dateBegin = rule.DateBegin.Date.ToString("yyyy-MM-ddZ"); | 
|  | 221 | +                var dateEnd = rule.DateEnd?.Date.ToString("yyyy-MM-ddZ") ?? "(no end)"; | 
|  | 222 | +                var ruleName = rule.Name; | 
|  | 223 | +                var content = JsonSerializer.Serialize(rule.ContentContainer.GetContentAs<object>()); | 
|  | 224 | + | 
|  | 225 | +                WriteLine($"{tab} | {lineNumber} | {priority,-8} | {active,-8} | {dateBegin,-11} - {dateEnd,-11} | {ruleName}: {content}"); | 
|  | 226 | +            } | 
|  | 227 | +        } | 
|  | 228 | +        else if (result.NumberOfRules > 0) | 
|  | 229 | +        { | 
|  | 230 | +            WriteLine($"{tab}{result.Rql}"); | 
|  | 231 | +            WriteLine($"{tab}{new string('-', result.Rql.Length)}"); | 
|  | 232 | +            WriteLine($"{tab} {result.NumberOfRules} rules were affected."); | 
|  | 233 | +        } | 
|  | 234 | +        else | 
|  | 235 | +        { | 
|  | 236 | +            WriteLine($"{tab}{result.Rql}"); | 
|  | 237 | +            WriteLine($"{tab}{new string('-', result.Rql.Length)}"); | 
|  | 238 | +            WriteLine($"{tab} (empty)"); | 
|  | 239 | +        } | 
|  | 240 | +    } | 
|  | 241 | + | 
|  | 242 | +    private void WriteLine(string? output = null) | 
|  | 243 | +    { | 
|  | 244 | +        if (output is not null) | 
|  | 245 | +        { | 
|  | 246 | +            var linesSplitOutput = output.Split('\n'); | 
|  | 247 | +            foreach (var line in linesSplitOutput) | 
|  | 248 | +            { | 
|  | 249 | +                this.outputQueue.Enqueue(line); | 
|  | 250 | +            } | 
|  | 251 | +        } | 
|  | 252 | +        else | 
|  | 253 | +        { | 
|  | 254 | +            this.outputQueue.Enqueue(string.Empty); | 
|  | 255 | +        } | 
|  | 256 | + | 
|  | 257 | +        while (this.outputQueue.Count >= this.Options.RqlTerminal.MaxOutputLines) | 
|  | 258 | +        { | 
|  | 259 | +            this.outputQueue.Dequeue(); | 
|  | 260 | +        } | 
|  | 261 | +    } | 
|  | 262 | +} | 
0 commit comments