@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

@(Id.HasValue ? $"Edit Shared Script: {_formName}" : "New Shared Script")

@if (_loading) { } else {
@* 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. *@
@if (_formError != null) {
@_formError
} @if (_syntaxCheckResult != null) {
@_syntaxCheckResult
}
@if (_showTestRun) {
Test Run Real I/O
Heads up: External, Database, and Notify calls fire for real against central's configured systems — real HTTP, real SQL, real emails. Side effects are permanent. CallShared executes the named shared script (saved version) in the same sandbox. Attributes and CallScript still throw.
@if (_runResult != null) { @_runResult.DurationMs ms }
@if (_runResult != null) { @if (_runResult.Success) {
@_runResult.ReturnValueJson
} else {
@_runResult.Error
@if (_runResult.Markers is { Count: > 0 }) {
    @foreach (var m in _runResult.Markers) {
  • Line @m.StartLineNumber, col @m.StartColumn: @m.Message @m.Code
  • }
}
} @if (!string.IsNullOrEmpty(_runResult.ConsoleOutput)) {
@_runResult.ConsoleOutput
} }
} }
@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 _markers = Array.Empty(); private bool _showTestRun; private bool _running; private Dictionary _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 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" }; /// /// Basic syntax check: balanced braces/brackets/parens. /// Mirrors the internal SharedScriptService.ValidateSyntax logic. /// 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; } }