Skip to content

Commit

Permalink
feat: add RQL terminal to Web UI
Browse files Browse the repository at this point in the history
  • Loading branch information
luispfgarces committed Nov 15, 2024
1 parent a3666a6 commit da6a4ff
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 2 deletions.
12 changes: 12 additions & 0 deletions samples/Rules.Framework.WebUI.Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace Rules.Framework.WebUI.Sample
{
using global::Rules.Framework.IntegrationTests.Common.Scenarios;
using global::Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8;
using global::Rules.Framework.WebUI.Sample.Engine;
using global::Rules.Framework.WebUI.Sample.ReadmeExample;
using global::Rules.Framework.WebUI.Sample.Rules;
Expand All @@ -23,6 +25,16 @@ public static void Main(string[] args)
}));

return await rulesProvider.GetRulesEngineAsync();
})
.AddInstance("Poker combinations example", async (_, _) =>
{
var rulesEngine = RulesEngineBuilder.CreateRulesEngine()
.SetInMemoryDataSource()
.Build();

await ScenarioLoader.LoadScenarioAsync(rulesEngine, new Scenario8Data());

return rulesEngine;
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Rules.Framework.WebUI\Rules.Framework.WebUI.csproj" />
<ProjectReference Include="..\..\src\Rules.Framework\Rules.Framework.csproj" />
<ProjectReference Include="..\..\tests\Rules.Framework.IntegrationTests.Common\Rules.Framework.IntegrationTests.Common.csproj" />
</ItemGroup>

</Project>
3 changes: 3 additions & 0 deletions src/Rules.Framework.WebUI/Assets/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,14 @@ h1:focus {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
}

main {
max-height: calc(100vh - 3.5rem);
height: calc(100vh - 3.5rem);
padding-right: 0 !important;
margin-right: 0 !important;
}

.sidebar {
Expand Down
6 changes: 6 additions & 0 deletions src/Rules.Framework.WebUI/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
</NavLink>
</div>

<div class="nav-item">
<NavLink class="nav-link" href="rules-ui/rql-terminal">
<span class="px-3"><Icon Name="IconName.Code" /> RQL Terminal</span>
</NavLink>
</div>

@if (this.shouldRenderSwitchInstanceLink)
{
<div class="nav-item">
Expand Down
262 changes: 262 additions & 0 deletions src/Rules.Framework.WebUI/Components/Pages/RqlTerminal.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
@attribute [ExcludeFromCodeCoverage]
@page "/rules-ui/rql-terminal"
@using Rules.Framework.Rql
@using Rules.Framework.Rql.Runtime.Types
@using System.Text
@using System.Text.Json
@rendermode InteractiveServer
@inject IJSRuntime JS
@inject WebUIOptions Options
@inject IRulesEngineInstanceProvider RulesEngineInstanceProvider
@inject ProtectedSessionStorage Storage

<PageTitle>@(this.Options.DocumentTitle) - RQL Terminal</PageTitle>

<h2>RQL Terminal</h2>

<div class="w-100" onclick="window.focusOnElement('commandInputTextbox')">
<div class="terminal p-2 bg-dark rounded shadow-sm d-flex flex-column-reverse overflow-auto">
<div class="terminal-text text-bg-dark d-flex">
<span id="commandInputLabel" class="pe-2">></span>
<input id="commandInputTextbox"
type="text"
class="terminal-input bg-dark text-bg-dark border-0 flex-fill"
aria-describedby="commandInputLabel"
@bind-value="commandInput"
@onkeyup="OnCommandInputKeyUpAsync" />
</div>
<pre class="terminal-output terminal-text text-bg-dark" onclick="window.focusOnElement('commandInputTextbox')">
@foreach (var line in this.outputQueue)
{
if (!string.IsNullOrWhiteSpace(line))
{
<code class="terminal-line mb-1" onclick="window.focusOnElement('commandInputTextbox')">@((MarkupString)line)</code>
}
<br class="mb-1" onclick="window.focusOnElement('commandInputTextbox')" />
}
</pre>
</div>
</div>

@code {
private static readonly string tab = new string(' ', 4);
private string commandInput;
private LinkedList<string> commandInputHistory;
private int commandInputHistoryCount;
private LinkedListNode<string> commandInputHistoryCurrent;
private Queue<string> outputQueue;
private IRqlEngine rqlEngine;

protected override void OnInitialized()
{
this.outputQueue = new Queue<string>(this.Options.RqlTerminal.MaxOutputLines);
this.commandInputHistory = new LinkedList<string>();
this.commandInputHistoryCount = 0;
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var instanceIdResult = await this.Storage.GetAsync<Guid>(WebUIConstants.SelectedInstanceStorageKey);
if (instanceIdResult.Success)
{
var instanceId = instanceIdResult.Value;
var rulesEngineInstance = this.RulesEngineInstanceProvider.GetInstance(instanceId);
this.rqlEngine = rulesEngineInstance.RulesEngine.GetRqlEngine();
this.StateHasChanged();
}
}

await this.JS.InvokeVoidAsync("scrollToTop", ".terminal > pre");
}

private async Task OnCommandInputKeyUpAsync(KeyboardEventArgs args)
{
switch (args.Key)
{
case "Enter":
case "NumpadEnter":
if (!string.IsNullOrWhiteSpace(this.commandInput))
{
await ExecuteAsync(this.rqlEngine, this.commandInput);

if (this.commandInputHistoryCount >= 50)
{
this.commandInputHistory.RemoveLast();
}

this.commandInputHistory.AddFirst(this.commandInput);
this.commandInput = string.Empty;
this.commandInputHistoryCurrent = null;
this.StateHasChanged();
}
break;

case "ArrowUp":
if (this.commandInputHistoryCurrent is not null)
{
if (this.commandInputHistoryCurrent.Next is not null)
{
this.commandInputHistoryCurrent = this.commandInputHistoryCurrent.Next;
this.commandInput = this.commandInputHistoryCurrent.Value;
}
}
else
{
this.commandInputHistoryCurrent = this.commandInputHistory.First;
if (this.commandInputHistoryCurrent is not null)
{
this.commandInput = this.commandInputHistoryCurrent.Value;
}
}
break;

case "ArrowDown":
if (this.commandInputHistoryCurrent is not null)
{
if (this.commandInputHistoryCurrent.Previous is not null)
{
this.commandInputHistoryCurrent = this.commandInputHistoryCurrent.Previous;
this.commandInput = this.commandInputHistoryCurrent.Value;
}
else
{
this.commandInput = string.Empty;
}
}
break;

default:
break;
}
}

private async Task ExecuteAsync(IRqlEngine rqlEngine, string? input)
{
try
{
WriteLine($"> {input}");
var results = await rqlEngine.ExecuteAsync(input);
foreach (var result in results)
{
switch (result)
{
case RulesSetResult rulesResultSet:
HandleRulesSetResult(rulesResultSet);
break;

case NothingResult:
// Nothing to be done.
break;

case ValueResult valueResult:
HandleObjectResult(valueResult);
break;

default:
throw new NotSupportedException($"Result type is not supported: '{result.GetType().FullName}'");
}
}
}
catch (RqlException rqlException)
{
WriteLine($"{rqlException.Message} Errors:");

foreach (var rqlError in rqlException.Errors)
{
var errorMessageBuilder = new StringBuilder(" - ")
.Append(rqlError.Text)
.Append(" @")
.Append(rqlError.BeginPosition)
.Append('-')
.Append(rqlError.EndPosition);
WriteLine(errorMessageBuilder.ToString());
}
}

WriteLine();
}

private void HandleObjectResult(ValueResult result)
{
WriteLine();
var rawValue = result.Value switch
{
RqlAny rqlAny when rqlAny.UnderlyingType == RqlTypes.Object => rqlAny.ToString() ?? string.Empty,
RqlAny rqlAny => rqlAny.ToString() ?? string.Empty,
_ => result.Value.ToString(),
};
var value = rawValue!.Replace("\n", $"\n{tab}");
WriteLine($"{tab}{value}");
}

private void HandleRulesSetResult(RulesSetResult result)
{
WriteLine();
if (result.Lines.Any())
{
WriteLine($"{tab}{result.Rql}");
WriteLine($"{tab}{new string('-', Math.Min(result.Rql.Length, 20))}");
if (result.NumberOfRules > 0)
{
WriteLine($"{tab} {result.NumberOfRules} rules were returned.");
}
else
{
WriteLine($"{tab} {result.Lines.Count} rules were returned.");
}

WriteLine();
WriteLine($"{tab} | # | Priority | Status | Range | Rule");
WriteLine($"{tab}{new string('-', 20)}");

foreach (var line in result.Lines)
{
var rule = line.Rule.Value;
var lineNumber = line.LineNumber.ToString();
var priority = rule.Priority.ToString();
var active = rule.Active ? "Active" : "Inactive";
var dateBegin = rule.DateBegin.Date.ToString("yyyy-MM-ddZ");
var dateEnd = rule.DateEnd?.Date.ToString("yyyy-MM-ddZ") ?? "(no end)";
var ruleName = rule.Name;
var content = JsonSerializer.Serialize(rule.ContentContainer.GetContentAs<object>());

WriteLine($"{tab} | {lineNumber} | {priority,-8} | {active,-8} | {dateBegin,-11} - {dateEnd,-11} | {ruleName}: {content}");
}
}
else if (result.NumberOfRules > 0)
{
WriteLine($"{tab}{result.Rql}");
WriteLine($"{tab}{new string('-', result.Rql.Length)}");
WriteLine($"{tab} {result.NumberOfRules} rules were affected.");
}
else
{
WriteLine($"{tab}{result.Rql}");
WriteLine($"{tab}{new string('-', result.Rql.Length)}");
WriteLine($"{tab} (empty)");
}
}

private void WriteLine(string? output = null)
{
if (output is not null)
{
var linesSplitOutput = output.Split('\n');
foreach (var line in linesSplitOutput)
{
this.outputQueue.Enqueue(line);
}
}
else
{
this.outputQueue.Enqueue(string.Empty);
}

while (this.outputQueue.Count >= this.Options.RqlTerminal.MaxOutputLines)
{
this.outputQueue.Dequeue();
}
}
}
27 changes: 27 additions & 0 deletions src/Rules.Framework.WebUI/Components/Pages/RqlTerminal.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.terminal {
height: 80vh !important;
scroll-behavior: smooth;
}

.terminal-input {
outline: none;
caret-color: rgb(255, 255, 255);
caret-shape: bar;
}

.terminal-line {
font-size: 1rem;
width: 100%;
word-wrap: break-word;
}

.terminal-output {
display: flex;
flex-direction: column;
white-space: pre-wrap;
}

.terminal-text {
font-family: var(--bs-font-monospace);
font-size: 1rem !important;
}
2 changes: 0 additions & 2 deletions src/Rules.Framework.WebUI/Components/Pages/SearchRules.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
@using System.IO
@using System.Text
@inject WebUIOptions Options
@inject IJSRuntime JS
@inject NavigationManager NavigationManager
@inject IRulesEngineInstanceProvider RulesEngineInstanceProvider
@inject ProtectedSessionStorage Storage

Expand Down
15 changes: 15 additions & 0 deletions src/Rules.Framework.WebUI/Components/WebUIApp.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<base href="/" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<link href="_content/Blazor.Bootstrap/blazor.bootstrap.css" rel="stylesheet" />
<link rel="stylesheet" href="rules-ui/glyphicons-only-bootstrap/css/bootstrap.css" />
<link rel="stylesheet" href="rules-ui/app.css" />
Expand All @@ -21,6 +22,7 @@
<Routes />
<script src="_framework/blazor.web.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="_content/Blazor.Bootstrap/blazor.bootstrap.js"></script>
<script>
window.downloadFileFromStream = async (fileName, contentStreamReference) => {
Expand All @@ -34,6 +36,19 @@
anchorElement.remove();
URL.revokeObjectURL(url);
}
window.scrollToTop = (selector) => {
let element = document.querySelector(selector);
element.scrollTo(0, element.scrollHeight);
}
window.focusOnElement = (id) => {
var selection = window.getSelection();
if (selection.type != "Range") {
document.getElementById(id).focus();
return false;
}
}
</script>
</body>
Expand Down
Loading

0 comments on commit da6a4ff

Please sign in to comment.