refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
@page "/design/shared-scripts/create"
|
||||
@page "/design/shared-scripts/{Id:int}/edit"
|
||||
@using ZB.MOM.WW.ScadaBridge.Security
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.TemplateEngine
|
||||
@using ScriptAnalysis = ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
|
||||
@inject ITemplateEngineRepository TemplateEngineRepository
|
||||
@inject SharedScriptService SharedScriptService
|
||||
@inject ScriptAnalysis.ScriptAnalysisService AnalysisService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="container-fluid mt-3">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm me-3" @onclick="GoBack">← Back</button>
|
||||
<h4 class="mb-0">@(Id.HasValue ? $"Edit Shared Script: {_formName}" : "New Shared Script")</h4>
|
||||
</div>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<LoadingSpinner IsLoading="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Name</label>
|
||||
<input type="text" class="form-control form-control-sm" @bind="_formName"
|
||||
disabled="@(Id.HasValue)" />
|
||||
</div>
|
||||
|
||||
@* Tabs: Code, Parameters, Return. Panels stay mounted (toggled
|
||||
via display:none) so the Monaco editor and the JSONJoy React
|
||||
island don't tear down on tab switch. *@
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button"
|
||||
class="nav-link @(_formTab == "code" ? "active" : "")"
|
||||
role="tab"
|
||||
aria-selected="@(_formTab == "code" ? "true" : "false")"
|
||||
@onclick='() => _formTab = "code"'>Code</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button"
|
||||
class="nav-link @(_formTab == "parameters" ? "active" : "")"
|
||||
role="tab"
|
||||
aria-selected="@(_formTab == "parameters" ? "true" : "false")"
|
||||
@onclick='() => _formTab = "parameters"'>Parameters</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button type="button"
|
||||
class="nav-link @(_formTab == "return" ? "active" : "")"
|
||||
role="tab"
|
||||
aria-selected="@(_formTab == "return" ? "true" : "false")"
|
||||
@onclick='() => _formTab = "return"'>Return type</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="border border-top-0 rounded-bottom p-3">
|
||||
<div style="display: @(_formTab == "code" ? "block" : "none")">
|
||||
<MonacoEditor @ref="_editor" Value="@_formCode" ValueChanged="@(v => _formCode = v)"
|
||||
Language="csharp" Height="320px"
|
||||
DeclaredParameters="@ScriptParameterNames.Parse(_formParameters)"
|
||||
DeclaredParameterShapes="@ScriptParameterNames.ParseShapes(_formParameters)"
|
||||
MarkersChanged="@(m => { _markers = m; StateHasChanged(); })" />
|
||||
<ProblemsPanel Markers="@_markers" OnNavigate="@(m => _editor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
|
||||
</div>
|
||||
<div style="display: @(_formTab == "parameters" ? "block" : "none")">
|
||||
<SchemaBuilder Mode="object"
|
||||
Value="@_formParameters"
|
||||
ValueChanged="@(v => _formParameters = v)" />
|
||||
</div>
|
||||
<div style="display: @(_formTab == "return" ? "block" : "none")">
|
||||
<SchemaBuilder Mode="value"
|
||||
Value="@_formReturn"
|
||||
ValueChanged="@(v => _formReturn = v)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_formError != null)
|
||||
{
|
||||
<div class="text-danger small mt-2">@_formError</div>
|
||||
}
|
||||
@if (_syntaxCheckResult != null)
|
||||
{
|
||||
<div class="@(_syntaxCheckPassed ? "text-success" : "text-danger") small mt-1">@_syntaxCheckResult</div>
|
||||
}
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success btn-sm me-1" @onclick="SaveScript">Save</button>
|
||||
<button class="btn btn-outline-info btn-sm me-1" @onclick="CheckCompilation">Check Syntax</button>
|
||||
<button class="btn btn-outline-primary btn-sm me-1" @onclick="ToggleTestRunPanel">
|
||||
@(_showTestRun ? "Hide Test Run" : "Test Run")
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="GoBack">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_showTestRun)
|
||||
{
|
||||
<div class="card mb-3" id="test-run-panel">
|
||||
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Test Run <span class="badge bg-warning text-dark ms-1">Real I/O</span></span>
|
||||
</div>
|
||||
<div class="alert alert-warning py-1 mb-0 small rounded-0 border-0 border-bottom">
|
||||
<strong>Heads up:</strong>
|
||||
<code>External</code>, <code>Database</code>, and <code>Notify</code> calls fire for real against central's configured systems — real HTTP, real SQL, real emails. Side effects are permanent.
|
||||
<code>CallShared</code> executes the named shared script (saved version) in the same sandbox.
|
||||
<code>Attributes</code> and <code>CallScript</code> still throw.
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Parameter values</label>
|
||||
<ParameterValueForm ParameterDefinitions="@_formParameters"
|
||||
Values="_paramValues"
|
||||
ValuesChanged="@(v => _paramValues = v)" />
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center mb-3">
|
||||
<button class="btn btn-primary btn-sm" @onclick="RunInSandboxAsync" disabled="@_running">
|
||||
@if (_running)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
<span>Running…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Run</span>
|
||||
}
|
||||
</button>
|
||||
@if (_runResult != null)
|
||||
{
|
||||
<span class="text-muted small">@_runResult.DurationMs ms</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (_runResult != null)
|
||||
{
|
||||
@if (_runResult.Success)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-success mb-1">
|
||||
Return value <span class="badge bg-light text-dark ms-1">@_runResult.ReturnTypeName</span>
|
||||
</label>
|
||||
<pre class="bg-light border rounded p-2 small mb-0 font-monospace" style="white-space: pre-wrap;">@_runResult.ReturnValueJson</pre>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-danger mb-1">
|
||||
<span class="badge bg-danger me-1">@ErrorKindLabel(_runResult.ErrorKind)</span>
|
||||
</label>
|
||||
<pre class="border border-danger-subtle rounded p-2 small mb-0 font-monospace text-danger" style="white-space: pre-wrap;">@_runResult.Error</pre>
|
||||
@if (_runResult.Markers is { Count: > 0 })
|
||||
{
|
||||
<ul class="small text-danger mt-2 mb-0">
|
||||
@foreach (var m in _runResult.Markers)
|
||||
{
|
||||
<li>Line @m.StartLineNumber, col @m.StartColumn: @m.Message <code class="ms-1">@m.Code</code></li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_runResult.ConsoleOutput))
|
||||
{
|
||||
<div class="mb-0">
|
||||
<label class="form-label small mb-1">Console output</label>
|
||||
<pre class="bg-dark text-light rounded p-2 small mb-0 font-monospace" style="white-space: pre-wrap;">@_runResult.ConsoleOutput</pre>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public int? Id { get; set; }
|
||||
|
||||
private bool _loading;
|
||||
private string _formTab = "code"; // "code" | "parameters" | "return"
|
||||
private string _formName = string.Empty;
|
||||
private string _formCode = string.Empty;
|
||||
private string? _formParameters;
|
||||
private string? _formReturn;
|
||||
private string? _formError;
|
||||
private string? _syntaxCheckResult;
|
||||
private bool _syntaxCheckPassed;
|
||||
private MonacoEditor? _editor;
|
||||
private IReadOnlyList<ScriptAnalysis.DiagnosticMarker> _markers = Array.Empty<ScriptAnalysis.DiagnosticMarker>();
|
||||
|
||||
private bool _showTestRun;
|
||||
private bool _running;
|
||||
private Dictionary<string, object?> _paramValues = new();
|
||||
private ScriptAnalysis.SandboxRunResult? _runResult;
|
||||
private CancellationTokenSource? _runCts;
|
||||
|
||||
// CentralUI-024: delegates to the shared helper so the claim type stays
|
||||
// resolved through JwtTokenService rather than a duplicated magic string.
|
||||
private Task<string> GetCurrentUserAsync()
|
||||
=> AuthStateProvider.GetCurrentUsernameAsync();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Id.HasValue)
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var scripts = await SharedScriptService.GetAllSharedScriptsAsync();
|
||||
var script = scripts.FirstOrDefault(s => s.Id == Id.Value);
|
||||
if (script != null)
|
||||
{
|
||||
_formName = script.Name;
|
||||
_formCode = script.Code;
|
||||
_formParameters = script.ParameterDefinitions;
|
||||
_formReturn = script.ReturnDefinition;
|
||||
}
|
||||
else
|
||||
{
|
||||
_formError = $"Shared script with ID {Id.Value} not found.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_formError = $"Failed to load script: {ex.Message}";
|
||||
}
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack()
|
||||
{
|
||||
NavigationManager.NavigateTo("/design/shared-scripts");
|
||||
}
|
||||
|
||||
private void CheckCompilation()
|
||||
{
|
||||
var syntaxError = ValidateSyntaxLocally(_formCode);
|
||||
if (syntaxError == null)
|
||||
{
|
||||
_syntaxCheckResult = "Syntax check passed.";
|
||||
_syntaxCheckPassed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_syntaxCheckResult = syntaxError;
|
||||
_syntaxCheckPassed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveScript()
|
||||
{
|
||||
_formError = null;
|
||||
_syntaxCheckResult = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (Id.HasValue)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await SharedScriptService.UpdateSharedScriptAsync(
|
||||
Id.Value, _formCode, _formParameters?.Trim(), _formReturn?.Trim(), user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
NavigationManager.NavigateTo("/design/shared-scripts");
|
||||
}
|
||||
else
|
||||
{
|
||||
_formError = result.Error;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
var result = await SharedScriptService.CreateSharedScriptAsync(
|
||||
_formName.Trim(), _formCode, _formParameters?.Trim(), _formReturn?.Trim(), user);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
NavigationManager.NavigateTo("/design/shared-scripts");
|
||||
}
|
||||
else
|
||||
{
|
||||
_formError = result.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_formError = $"Save failed: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleTestRunPanel()
|
||||
{
|
||||
_showTestRun = !_showTestRun;
|
||||
}
|
||||
|
||||
private async Task RunInSandboxAsync()
|
||||
{
|
||||
_runCts?.Cancel();
|
||||
_runCts = new CancellationTokenSource();
|
||||
_running = true;
|
||||
_runResult = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var jsonParams = _paramValues.ToDictionary(
|
||||
kv => kv.Key,
|
||||
kv => System.Text.Json.JsonSerializer.SerializeToElement(kv.Value));
|
||||
var request = new ScriptAnalysis.SandboxRunRequest(_formCode, jsonParams, TimeoutSeconds: null);
|
||||
_runResult = await AnalysisService.RunInSandboxAsync(request, _runCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) { /* superseded by next Run click */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_runResult = new ScriptAnalysis.SandboxRunResult(
|
||||
Success: false,
|
||||
ReturnValueJson: null,
|
||||
ReturnTypeName: null,
|
||||
ConsoleOutput: "",
|
||||
Error: $"Unexpected: {ex.GetType().Name}: {ex.Message}",
|
||||
ErrorKind: ScriptAnalysis.SandboxErrorKind.RuntimeError,
|
||||
DurationMs: 0,
|
||||
Markers: null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_running = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ErrorKindLabel(ScriptAnalysis.SandboxErrorKind kind) => kind switch
|
||||
{
|
||||
ScriptAnalysis.SandboxErrorKind.CompileError => "Compile error",
|
||||
ScriptAnalysis.SandboxErrorKind.SandboxLimitation => "Sandbox limitation",
|
||||
ScriptAnalysis.SandboxErrorKind.RuntimeError => "Runtime error",
|
||||
ScriptAnalysis.SandboxErrorKind.Timeout => "Timeout",
|
||||
_ => "Error"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Basic syntax check: balanced braces/brackets/parens.
|
||||
/// Mirrors the internal SharedScriptService.ValidateSyntax logic.
|
||||
/// </summary>
|
||||
private static string? ValidateSyntaxLocally(string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code)) return "Script code cannot be empty.";
|
||||
int brace = 0, bracket = 0, paren = 0;
|
||||
foreach (var ch in code)
|
||||
{
|
||||
switch (ch) { case '{': brace++; break; case '}': brace--; break; case '[': bracket++; break; case ']': bracket--; break; case '(': paren++; break; case ')': paren--; break; }
|
||||
if (brace < 0) return "Syntax error: unmatched closing brace '}'.";
|
||||
if (bracket < 0) return "Syntax error: unmatched closing bracket ']'.";
|
||||
if (paren < 0) return "Syntax error: unmatched closing parenthesis ')'.";
|
||||
}
|
||||
if (brace != 0) return "Syntax error: unmatched opening brace '{'.";
|
||||
if (bracket != 0) return "Syntax error: unmatched opening bracket '['.";
|
||||
if (paren != 0) return "Syntax error: unmatched opening parenthesis '('.";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user