fix(uns): enforce the UNS segment rule on every authoring path (#535)
An area or line could be named "Packaging" or "Line 1", save cleanly, and then fail the NEXT deployment with UnsSegmentInvalid. Because a deployment snapshots the whole database, one badly-named area blocked EVERY cluster's deploy - and whoever hit that was usually not whoever typed the name. Only the equipment form carried the rule; the deploy gate held its own private copy. The rule now lives once, on Configuration.Validation.UnsSegment, and both ends call it. DraftValidator no longer restates it. Enforced in UnsTreeService, not just on the forms: - Create/Update Area, Line and Equipment reject an invalid name before touching the row. - The CSV import reports a bad name as an ERROR, not a skip - a skip means "you already have this row", which would tell the operator the equipment exists when it does not. Good rows in the same batch still insert. A form attribute could never have covered the import, and with no bUnit in this repo a rule only a form enforces is also a rule no test can reach. The forms keep an attribute for the immediate message, but it now references the shared pattern rather than restating it - a second copy is how these drifted apart. One behaviour change beyond the fix: UnsSegment.AttributePattern admits "_default", which the equipment form's hand-written literal rejected. The form was stricter than the gate it was mirroring, so a _default row was uneditable through the AdminUI. Tests: 41, falsified with three production mutations - dropping the CreateArea guard reds exactly the CreateArea rejections, turning the import error back into a skip reds only the import test, and narrowing AttributePattern reds only the attribute/IsValid agreement. Every rejection is paired with a name that must still be accepted, or a guard that refused everything would pass. Live-verified on docker-dev: the area modal now refuses "Packaging" in place with the rule spelled out, "packaging-b" saves, and a CSV import of two rows reports Inserted 1 / Skipped 0 / Errors 1 naming the offending row - the path no form attribute reaches. Rig cleaned up and a further deployment sealed. Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Validation
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns
|
||||
@inject IUnsTreeService Svc
|
||||
@@ -567,7 +568,10 @@ else
|
||||
/// <summary>The working form for the Details tab — identical to EquipmentModal.FormModel.</summary>
|
||||
private sealed class FormModel
|
||||
{
|
||||
[Required, RegularExpression("^[a-z0-9-]{1,32}$", ErrorMessage = "Lowercase letters, digits, dashes only; max 32 chars.")]
|
||||
// The shared rule, not a local restatement — a second copy is how the UI and the deploy gate
|
||||
// drifted apart in the first place (Gitea #535). AttributePattern also admits "_default", which
|
||||
// the previous literal rejected, making the form stricter than the gate it was mirroring.
|
||||
[Required, RegularExpression(UnsSegment.AttributePattern, ErrorMessage = UnsSegment.Message)]
|
||||
public string Name { get; set; } = "";
|
||||
[Required] public string MachineCode { get; set; } = "";
|
||||
[Required] public string UnsLineId { get; set; } = "";
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
served-by cluster list. On a successful save it raises OnSaved so the host can reload the tree. *@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Validation
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@inject IUnsTreeService Svc
|
||||
|
||||
@@ -29,6 +30,7 @@
|
||||
<label class="form-label" for="area-name">Name</label>
|
||||
<InputText id="area-name" @bind-Value="_form.Name" class="form-control form-control-sm" />
|
||||
<ValidationMessage For="@(() => _form.Name)" />
|
||||
<div class="form-text">UNS path segment: lowercase letters, digits and dashes, 1-32 characters. It becomes a level of the OPC UA NodeId.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="area-cluster">Served by cluster</label>
|
||||
@@ -138,7 +140,8 @@
|
||||
private sealed class FormModel
|
||||
{
|
||||
[Required, RegularExpression("^[A-Za-z0-9_-]+$")] public string UnsAreaId { get; set; } = "";
|
||||
[Required] public string Name { get; set; } = "";
|
||||
[Required, RegularExpression(UnsSegment.AttributePattern, ErrorMessage = UnsSegment.Message)]
|
||||
public string Name { get; set; } = "";
|
||||
[Required] public string ClusterId { get; set; } = "";
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
On a successful save it raises OnSaved so the host can reload the tree. *@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Validation
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@inject IUnsTreeService Svc
|
||||
|
||||
@@ -40,6 +41,7 @@
|
||||
<label class="form-label" for="line-name">Name</label>
|
||||
<InputText id="line-name" @bind-Value="_form.Name" class="form-control form-control-sm" />
|
||||
<ValidationMessage For="@(() => _form.Name)" />
|
||||
<div class="form-text">UNS path segment: lowercase letters, digits and dashes, 1-32 characters. It becomes a level of the OPC UA NodeId.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="line-notes">Notes</label>
|
||||
@@ -140,7 +142,8 @@
|
||||
{
|
||||
[Required, RegularExpression("^[A-Za-z0-9_-]+$")] public string UnsLineId { get; set; } = "";
|
||||
[Required] public string UnsAreaId { get; set; } = "";
|
||||
[Required] public string Name { get; set; } = "";
|
||||
[Required, RegularExpression(UnsSegment.AttributePattern, ErrorMessage = UnsSegment.Message)]
|
||||
public string Name { get; set; } = "";
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user