2124f21ab6
v2-ci / build (pull_request) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (pull_request) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (pull_request) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (pull_request) Has been skipped
HistorianGateway is now the sole historian backend (read + alarm SendEvent + continuous WriteLiveValues). Document the final state and retire the Wonderware sidecar from the docs/config/labels: - CLAUDE.md: rewrite the Historian section — ServerHistorian / ContinuousHistorization / AlarmHistorian config keys, the IHistorianProvisioning EnsureTags hook, the GatewayAlarmHistorianWriter SendEvent path + ReadEvents dependency on gateway RuntimeDb:EventReadsEnabled=true, gateway-side prerequisites (RuntimeDb flags + historian:read/write/tags:write scopes), migration note, and two KNOWN-LIMITATION callouts (live-validation gate + empty historized-ref-set recorder follow-on). - appsettings.json: fix the stale ServerHistorian block (Host/Port/SharedSecret/ ServerCertThumbprint -> Endpoint/ApiKey/UseTls/AllowUntrustedServerCertificate/ CaCertificatePath/CallTimeout, keep MaxTieClusterOverfetch); add a disabled ContinuousHistorization block; prune the orphaned Wonderware keys from AlarmHistorian (keep the SQLite knobs). ApiKey env-supplied via ServerHistorian__ApiKey (commented; valid strict JSON via _comment keys). - README.md + docs (Historian.md, AlarmHistorian.md, Configuration.md, ServiceHosting.md, DriverLifecycle.md, drivers/README.md, Uns.md, VirtualTags.md, AlarmTracking.md, Client.UI.md, README.md, TestConnectProbes.md): retire the Wonderware historian backend from current-backend descriptions; fix the stale ServerHistorian/AlarmHistorian config tables (now gateway shape); convert drivers/Historian.Wonderware.md to a retired stub pointing at the gateway. - Source/UI labels (descriptive text only, no behavior change): OtOpcUaServerHostedService.cs, HistoryPaging.cs, OtOpcUaSdkServer.cs, HistorianAdapterActor.cs, VirtualTagModal.razor, ScriptedAlarmModal.razor, AlarmsHistorian.razor now name the HistorianGateway backend. Build clean (0 errors); AdminUI.Tests green (514 passed). Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
225 lines
12 KiB
Plaintext
225 lines
12 KiB
Plaintext
@* Create/edit modal for an equipment-bound scripted alarm, wired straight into IUnsTreeService. The host
|
|
page owns visibility and supplies the owning equipment id (create) or the loaded ScriptedAlarmEditDto
|
|
(edit), plus the candidate predicate-script list. The owning equipment is fixed by the tab, so there is
|
|
no equipment selector. On a successful save it raises OnSaved so the host can refresh the equipment's
|
|
alarms in place. Lifted from the standalone ScriptedAlarmEdit page (retired in a later task). *@
|
|
@using System.ComponentModel.DataAnnotations
|
|
@using Microsoft.AspNetCore.Components.Forms
|
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
|
@inject IUnsTreeService Svc
|
|
|
|
@if (Visible)
|
|
{
|
|
<div class="modal-backdrop fade show" style="display:block"></div>
|
|
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<EditForm Model="_form" OnValidSubmit="SaveAsync" FormName="scriptedAlarmModal">
|
|
<DataAnnotationsValidator />
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">@(IsNew ? "New scripted alarm" : "Edit scripted alarm")</h5>
|
|
<button type="button" class="btn-close" aria-label="Close" @onclick="CancelAsync"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label" for="alarm-id">ScriptedAlarmId</label>
|
|
<InputText id="alarm-id" @bind-Value="_form.ScriptedAlarmId" disabled="@(!IsNew)"
|
|
class="form-control form-control-sm mono" />
|
|
<ValidationMessage For="@(() => _form.ScriptedAlarmId)" />
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label" for="alarm-name">Name</label>
|
|
<InputText id="alarm-name" @bind-Value="_form.Name" class="form-control form-control-sm" />
|
|
<ValidationMessage For="@(() => _form.Name)" />
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label" for="alarm-type">AlarmType</label>
|
|
<InputSelect id="alarm-type" @bind-Value="_form.AlarmType" class="form-select form-select-sm">
|
|
<option value="LimitAlarm">LimitAlarm</option>
|
|
<option value="DiscreteAlarm">DiscreteAlarm</option>
|
|
<option value="OffNormalAlarm">OffNormalAlarm</option>
|
|
<option value="AlarmCondition">AlarmCondition</option>
|
|
</InputSelect>
|
|
<ValidationMessage For="@(() => _form.AlarmType)" />
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label" for="alarm-severity">Severity (1-1000)</label>
|
|
<InputNumber id="alarm-severity" @bind-Value="_form.Severity" class="form-control form-control-sm" />
|
|
<ValidationMessage For="@(() => _form.Severity)" />
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" for="alarm-script">Predicate script</label>
|
|
<InputSelect id="alarm-script" @bind-Value="_form.PredicateScriptId" class="form-select form-select-sm">
|
|
<option value="">— pick script —</option>
|
|
@foreach (var (id, display) in Scripts)
|
|
{
|
|
<option value="@id">@display</option>
|
|
}
|
|
</InputSelect>
|
|
<ValidationMessage For="@(() => _form.PredicateScriptId)" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" for="alarm-message">Message template</label>
|
|
<InputTextArea id="alarm-message" @bind-Value="_form.MessageTemplate" rows="3"
|
|
class="form-control form-control-sm"
|
|
placeholder="{equipment.MachineCode} temperature out of range: {value}°C" />
|
|
<ValidationMessage For="@(() => _form.MessageTemplate)" />
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">HistorizeToAveva</label>
|
|
<div class="form-check form-switch">
|
|
<InputCheckbox @bind-Value="_form.HistorizeToAveva" class="form-check-input" />
|
|
<label class="form-check-label">Route to HistorianGateway</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Retain</label>
|
|
<div class="form-check form-switch">
|
|
<InputCheckbox @bind-Value="_form.Retain" class="form-check-input" />
|
|
<label class="form-check-label">Retain active alarms on restart</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Enabled</label>
|
|
<div class="form-check form-switch">
|
|
<InputCheckbox @bind-Value="_form.Enabled" class="form-check-input" />
|
|
<label class="form-check-label">Spawn this alarm in deployments</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(_error))
|
|
{
|
|
<div class="text-danger small mt-2">@_error</div>
|
|
}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" @onclick="CancelAsync" disabled="@_busy">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" disabled="@_busy">
|
|
@if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
|
@(IsNew ? "Create" : "Save changes")
|
|
</button>
|
|
</div>
|
|
</EditForm>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
/// <summary>Whether the modal is shown. The host owns this flag.</summary>
|
|
[Parameter] public bool Visible { get; set; }
|
|
|
|
/// <summary><c>true</c> to create a new scripted alarm; <c>false</c> to edit <see cref="Existing"/>.</summary>
|
|
[Parameter] public bool IsNew { get; set; }
|
|
|
|
/// <summary>The owning equipment id the created alarm binds to (used only on create).</summary>
|
|
[Parameter] public string? EquipmentId { get; set; }
|
|
|
|
/// <summary>The scripted alarm being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
|
|
[Parameter] public ScriptedAlarmEditDto? Existing { get; set; }
|
|
|
|
/// <summary>The candidate predicate scripts as <c>(Id, Display)</c> pairs.</summary>
|
|
[Parameter] public IReadOnlyList<(string Id, string Display)> Scripts { get; set; } = Array.Empty<(string, string)>();
|
|
|
|
/// <summary>Raised after a successful create/save so the host can refresh the equipment's alarms and close.</summary>
|
|
[Parameter] public EventCallback OnSaved { get; set; }
|
|
|
|
/// <summary>Raised when the user cancels so the host can close.</summary>
|
|
[Parameter] public EventCallback OnCancel { get; set; }
|
|
|
|
private FormModel _form = new();
|
|
private bool _busy;
|
|
private string? _error;
|
|
|
|
// Tracks which open this modal last loaded for, so unrelated Blazor Server re-renders don't
|
|
// rebuild _form and clobber in-progress edits. Null while closed.
|
|
private string? _loadedKey;
|
|
|
|
protected override void OnParametersSet()
|
|
{
|
|
if (!Visible)
|
|
{
|
|
_loadedKey = null; // closed → next open reloads fresh
|
|
return;
|
|
}
|
|
|
|
// Guard against unrelated re-renders. In Blazor Server any live-status push re-invokes
|
|
// OnParametersSet; without this the rebuild below would silently discard whatever the
|
|
// operator has typed. Only rebuild when the modal OPENS or the target entity CHANGES.
|
|
var key = IsNew ? "<new>" : Existing?.ScriptedAlarmId;
|
|
if (key == _loadedKey) return; // same open, re-render → preserve in-progress form edits
|
|
_loadedKey = key;
|
|
|
|
// Rebuild the working form whenever the host (re)opens the modal for a fresh target.
|
|
if (IsNew)
|
|
{
|
|
_form = new FormModel();
|
|
}
|
|
else if (Existing is not null)
|
|
{
|
|
_form = new FormModel
|
|
{
|
|
ScriptedAlarmId = Existing.ScriptedAlarmId,
|
|
Name = Existing.Name,
|
|
AlarmType = Existing.AlarmType,
|
|
Severity = Existing.Severity,
|
|
MessageTemplate = Existing.MessageTemplate,
|
|
PredicateScriptId = Existing.PredicateScriptId,
|
|
HistorizeToAveva = Existing.HistorizeToAveva,
|
|
Retain = Existing.Retain,
|
|
Enabled = Existing.Enabled,
|
|
};
|
|
}
|
|
_error = null;
|
|
}
|
|
|
|
private async Task SaveAsync()
|
|
{
|
|
_busy = true;
|
|
_error = null;
|
|
try
|
|
{
|
|
var input = new ScriptedAlarmInput(_form.ScriptedAlarmId, _form.Name, _form.AlarmType, _form.Severity,
|
|
_form.MessageTemplate, _form.PredicateScriptId, _form.HistorizeToAveva, _form.Retain, _form.Enabled);
|
|
|
|
var result = IsNew
|
|
? await Svc.CreateScriptedAlarmAsync(EquipmentId!, input)
|
|
: await Svc.UpdateScriptedAlarmAsync(Existing!.ScriptedAlarmId, input, Existing.RowVersion);
|
|
|
|
if (result.Ok)
|
|
{
|
|
await OnSaved.InvokeAsync();
|
|
}
|
|
else
|
|
{
|
|
_error = result.Error;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_busy = false;
|
|
}
|
|
}
|
|
|
|
private Task CancelAsync() => OnCancel.InvokeAsync();
|
|
|
|
private sealed class FormModel
|
|
{
|
|
[Required, RegularExpression("^[A-Za-z0-9_-]+$")] public string ScriptedAlarmId { get; set; } = "";
|
|
[Required] public string Name { get; set; } = "";
|
|
[Required] public string AlarmType { get; set; } = "LimitAlarm";
|
|
[Range(1, 1000)] public int Severity { get; set; } = 500;
|
|
[Required] public string PredicateScriptId { get; set; } = "";
|
|
[Required] public string MessageTemplate { get; set; } = "";
|
|
public bool HistorizeToAveva { get; set; } = true;
|
|
public bool Retain { get; set; } = true;
|
|
public bool Enabled { get; set; } = true;
|
|
}
|
|
}
|