9e243493fb
Applies the family-wide admin-UI cleanup playbook to the Central UI so the Blazor surfaces stop diverging from the shared kit: buttons are grouped rather than individually sized, long cell values are contained instead of widening tables, and hard-coded colours give way to theme tokens. The headline fix is that MainLayout passed Accent="#2f5fd0" to ThemeShell, which the kit emits as an inline style on the shell root. Being a descendant of <html>, it beat the [data-bs-theme="dark"] override for the entire app, so the dark accent had never rendered. Declaring --accent in site.css :root instead lets both schemes resolve; light is unchanged because the value already matched the kit's light default. Theme pins to 0.4.1, which upstreams the local .btn sizing block verbatim, so that block is deleted here rather than duplicated. Verified byte-identical before removal; the repo now declares no --bs-btn-* anywhere. NOT purely cosmetic, contrary to the sweep's stated scope: four detail-modal surfaces (NotificationReport, ConfigurationAuditLog, ParkedMessages, SiteCallsReport) were additionally refactored from holding the selected record to holding its id and re-resolving from the current page each render, with the resolve doubling as the visibility gate. A background refresh that drops the row now closes the modal instead of showing a stale snapshot. This is a behaviour change and is called out rather than buried: a full-suite run turned up one intermittent CentralUI failure, CloseButton_DismissesModal, whose stack (GetRequiredEventBindingEntry during DispatchEventAsync) indicates the handler was disposed between render and click — a window the previous field-held record made structurally impossible. Treat the modal lifecycle here as unreviewed. Build 0/0; suite green apart from that one intermittent failure.
261 lines
11 KiB
Plaintext
261 lines
11 KiB
Plaintext
@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
|
|
@implements IAsyncDisposable
|
|
@inject IJSRuntime JS
|
|
@inject Microsoft.Extensions.Logging.ILogger<MonacoEditor> Logger
|
|
|
|
@if (ShowToolbar)
|
|
{
|
|
@* Four editor-state toggles: actions on the editor, not navigation — so they
|
|
are outline buttons in one segmented group, not link buttons. *@
|
|
<div class="d-flex justify-content-end align-items-center mb-1">
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button type="button" class="btn btn-outline-secondary" @onclick="FormatAsync"
|
|
title="Format document (Ctrl/Cmd+Shift+F)">Format</button>
|
|
<button type="button" class="btn btn-outline-secondary" @onclick="ToggleWrap"
|
|
title="Word wrap">@(_wrap ? "Wrap on" : "Wrap off")</button>
|
|
<button type="button" class="btn btn-outline-secondary" @onclick="ToggleMinimap"
|
|
title="Toggle minimap">@(_minimap ? "Minimap on" : "Minimap off")</button>
|
|
<button type="button" class="btn btn-outline-secondary" @onclick="ToggleTheme"
|
|
title="Toggle theme">@(_dark ? "Dark" : "Light")</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div @ref="_hostRef" class="monaco-editor-host"
|
|
style="height: @Height; border: 1px solid var(--bs-border-color); border-radius: 0.25rem; overflow: hidden;"></div>
|
|
|
|
@code {
|
|
[Parameter] public string Value { get; set; } = "";
|
|
[Parameter] public EventCallback<string> ValueChanged { get; set; }
|
|
[Parameter] public string Language { get; set; } = "csharp";
|
|
[Parameter] public string Height { get; set; } = "320px";
|
|
[Parameter] public bool ReadOnly { get; set; } = false;
|
|
[Parameter] public bool ShowToolbar { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Optional JSON Schema (the resolved schema text — any
|
|
/// <c>{"$ref":"lib:Name"}</c> already inlined) applied to this editor's
|
|
/// model when <see cref="Language"/> is <c>json</c>. Wires Monaco's
|
|
/// built-in JSON language so the editor offers schema-driven hover,
|
|
/// completion, and diagnostics on the entered JSON. Ignored for non-JSON
|
|
/// languages. Updating it re-applies the schema to the live model.
|
|
/// </summary>
|
|
[Parameter] public string? JsonSchema { get; set; }
|
|
|
|
/// <summary>
|
|
/// Runtime globals surface the script is analyzed against. Defaults to
|
|
/// template/shared-script globals; set to <c>InboundApi</c> on the API
|
|
/// method editor so <c>Route</c> and <c>Parameters</c> type-check.
|
|
/// </summary>
|
|
[Parameter] public ScriptAnalysis.ScriptKind ScriptKind { get; set; } = ScriptAnalysis.ScriptKind.Template;
|
|
|
|
/// <summary>
|
|
/// Parameter names declared on the form (derived from the SchemaBuilder's
|
|
/// JSON Schema), surfaced as completions inside Parameters["..."] literals
|
|
/// and used by the unknown-key diagnostic.
|
|
/// </summary>
|
|
[Parameter] public IReadOnlyList<string>? DeclaredParameters { get; set; }
|
|
|
|
/// <summary>
|
|
/// Full shapes (name + type + required) for the declared parameters.
|
|
/// Used by Parameters["name"] hover to show the declared type. If null,
|
|
/// derived from <see cref="DeclaredParameters"/> with type "Object".
|
|
/// </summary>
|
|
[Parameter] public IReadOnlyList<ScriptAnalysis.ParameterShape>? DeclaredParameterShapes { get; set; }
|
|
|
|
/// <summary>
|
|
/// Shapes (name + parameter list + return type) of other scripts on the
|
|
/// same template. Surfaced inside CallScript("...") for completion,
|
|
/// signature help, hover, and argument-count diagnostics.
|
|
/// </summary>
|
|
[Parameter] public IReadOnlyList<ScriptAnalysis.ScriptShape>? SiblingScripts { get; set; }
|
|
|
|
/// <summary>
|
|
/// Attributes declared on the current template. Surfaced inside
|
|
/// <c>Attributes["..."]</c> for completion and SCADA006 diagnostics.
|
|
/// </summary>
|
|
[Parameter] public IReadOnlyList<ScriptAnalysis.AttributeShape>? SelfAttributes { get; set; }
|
|
|
|
/// <summary>
|
|
/// Child compositions on the current template, each with its template's
|
|
/// attributes and scripts. Surfaced for <c>Children["X"].Attributes</c>,
|
|
/// <c>Children["X"].CallScript</c>, and SCADA007 diagnostics.
|
|
/// </summary>
|
|
[Parameter] public IReadOnlyList<ScriptAnalysis.CompositionContext>? Children { get; set; }
|
|
|
|
/// <summary>
|
|
/// Parent template when the current template is composed inside exactly
|
|
/// one other template. <c>null</c> at the root or when multiple parents
|
|
/// exist. Surfaced for <c>Parent.Attributes</c> / <c>Parent.CallScript</c>.
|
|
/// </summary>
|
|
[Parameter] public ScriptAnalysis.CompositionContext? Parent { get; set; }
|
|
|
|
/// <summary>
|
|
/// Fires whenever Monaco's marker set updates (after the 500 ms diagnostic
|
|
/// debounce). Hosts can render a <see cref="ProblemsPanel"/> with the same
|
|
/// data.
|
|
/// </summary>
|
|
[Parameter] public EventCallback<IReadOnlyList<ScriptAnalysis.DiagnosticMarker>> MarkersChanged { get; set; }
|
|
|
|
private ElementReference _hostRef;
|
|
private DotNetObjectReference<MonacoEditor>? _dotNetRef;
|
|
private readonly string _id = Guid.NewGuid().ToString("N");
|
|
private string _lastSentValue = "";
|
|
private string? _lastSentSchema;
|
|
private bool _initialized;
|
|
private bool _wrap;
|
|
private bool _minimap;
|
|
private bool _dark;
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
_dotNetRef = DotNetObjectReference.Create(this);
|
|
_lastSentValue = Value ?? "";
|
|
try
|
|
{
|
|
await JS.InvokeVoidAsync(
|
|
"MonacoBlazor.createEditor",
|
|
_id,
|
|
_hostRef,
|
|
new
|
|
{
|
|
value = Value ?? "",
|
|
language = Language,
|
|
readOnly = ReadOnly,
|
|
jsonSchema = JsonSchema
|
|
},
|
|
_dotNetRef);
|
|
_initialized = true;
|
|
_lastSentSchema = JsonSchema;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
// Prerendering: JS interop is not available yet — the next
|
|
// (interactive) render retries. Expected, not logged.
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// Circuit disconnected before init completed — nothing to do.
|
|
}
|
|
catch (JSException ex)
|
|
{
|
|
// A genuine Monaco init failure — surface it instead of hiding it.
|
|
Logger.LogError(ex, "Monaco editor {EditorId} failed to initialize.", _id);
|
|
}
|
|
}
|
|
else if (_initialized)
|
|
{
|
|
if ((Value ?? "") != _lastSentValue)
|
|
{
|
|
_lastSentValue = Value ?? "";
|
|
await SafeInvokeAsync("MonacoBlazor.setValue", "set editor value", _id, _lastSentValue);
|
|
}
|
|
if (JsonSchema != _lastSentSchema)
|
|
{
|
|
_lastSentSchema = JsonSchema;
|
|
await SafeInvokeAsync("MonacoBlazor.setJsonSchema", "set JSON schema", _id, JsonSchema);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Invokes a Monaco JS function, swallowing the expected disconnect case but
|
|
/// logging any genuine JS error (CentralUI-018) so failures are not silent.
|
|
/// </summary>
|
|
private async ValueTask SafeInvokeAsync(string fn, string action, params object?[] args)
|
|
{
|
|
try
|
|
{
|
|
await JS.InvokeVoidAsync(fn, args);
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// Circuit gone — the editor no longer exists; nothing to log.
|
|
}
|
|
catch (JSException ex)
|
|
{
|
|
Logger.LogWarning(ex, "Monaco editor {EditorId}: failed to {Action}.", _id, action);
|
|
}
|
|
}
|
|
|
|
[JSInvokable]
|
|
public Task OnValueChanged(string newValue)
|
|
{
|
|
_lastSentValue = newValue ?? "";
|
|
return ValueChanged.InvokeAsync(_lastSentValue);
|
|
}
|
|
|
|
[JSInvokable]
|
|
public Task OnMarkersChanged(ScriptAnalysis.DiagnosticMarker[] markers) =>
|
|
MarkersChanged.InvokeAsync(markers ?? Array.Empty<ScriptAnalysis.DiagnosticMarker>());
|
|
|
|
/// <summary>Programmatic scroll-to-line (called by the problems panel).</summary>
|
|
public async Task RevealLineAsync(int line, int column = 1)
|
|
{
|
|
if (!_initialized) return;
|
|
await SafeInvokeAsync("MonacoBlazor.revealLine", "reveal line", _id, line, column);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called from JS at completion-request time so the form's latest state is
|
|
/// passed through, not whatever was captured when the editor was created.
|
|
/// </summary>
|
|
[JSInvokable]
|
|
public ScadaContext GetContext() => new(
|
|
DeclaredParameters?.ToArray() ?? Array.Empty<string>(),
|
|
SiblingScripts?.ToArray() ?? Array.Empty<ScriptAnalysis.ScriptShape>(),
|
|
DeclaredParameterShapes?.ToArray()
|
|
?? DeclaredParameters?.Select(n => new ScriptAnalysis.ParameterShape(n, "Object", true)).ToArray()
|
|
?? Array.Empty<ScriptAnalysis.ParameterShape>(),
|
|
SelfAttributes?.ToArray() ?? Array.Empty<ScriptAnalysis.AttributeShape>(),
|
|
Children?.ToArray() ?? Array.Empty<ScriptAnalysis.CompositionContext>(),
|
|
Parent,
|
|
ScriptKind);
|
|
|
|
private async Task FormatAsync()
|
|
{
|
|
if (!_initialized) return;
|
|
await SafeInvokeAsync("MonacoBlazor.format", "format document", _id);
|
|
}
|
|
|
|
private async Task ToggleWrap()
|
|
{
|
|
_wrap = !_wrap;
|
|
await SafeInvokeAsync("MonacoBlazor.setEditorOption", "toggle word wrap", _id, "wordWrap", _wrap ? "on" : "off");
|
|
}
|
|
|
|
private async Task ToggleMinimap()
|
|
{
|
|
_minimap = !_minimap;
|
|
await SafeInvokeAsync("MonacoBlazor.setEditorOption", "toggle minimap", _id, "minimap", new { enabled = _minimap });
|
|
}
|
|
|
|
private async Task ToggleTheme()
|
|
{
|
|
_dark = !_dark;
|
|
await SafeInvokeAsync("MonacoBlazor.setEditorOption", "toggle theme", _id, "theme", _dark ? "vs-dark" : "vs");
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_initialized)
|
|
{
|
|
// Disposal commonly races a circuit disconnect — JSDisconnectedException
|
|
// here is expected and silent; a real JSException is still logged.
|
|
await SafeInvokeAsync("MonacoBlazor.dispose", "dispose editor", _id);
|
|
}
|
|
_dotNetRef?.Dispose();
|
|
}
|
|
|
|
public record ScadaContext(
|
|
string[] DeclaredParameters,
|
|
ScriptAnalysis.ScriptShape[] SiblingScripts,
|
|
ScriptAnalysis.ParameterShape[] DeclaredParameterShapes,
|
|
ScriptAnalysis.AttributeShape[] SelfAttributes,
|
|
ScriptAnalysis.CompositionContext[] Children,
|
|
ScriptAnalysis.CompositionContext? Parent,
|
|
ScriptAnalysis.ScriptKind ScriptKind);
|
|
}
|