feat(uns): tag + virtual-tag modals wired into the tree
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
@* Create/edit modal for an equipment-bound tag, wired straight into IUnsTreeService. The host page
|
||||
owns visibility and supplies the owning equipment id (create) or the loaded TagEditDto (edit), plus
|
||||
the equipment-scoped candidate-driver list. Tree tags are always equipment-bound (decision #110), so
|
||||
the legacy SystemPlatform/FolderPath branch is dropped entirely — there is no equipment selector
|
||||
either, the owning equipment is fixed. On a successful save it raises OnSaved so the host can
|
||||
refresh the equipment's children in place. *@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||
@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="tagModal">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(IsNew ? "New tag" : "Edit tag")</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="tag-id">TagId</label>
|
||||
<InputText id="tag-id" @bind-Value="_form.TagId" disabled="@(!IsNew)"
|
||||
class="form-control form-control-sm mono"
|
||||
placeholder="tag-line3-temp-01" />
|
||||
<ValidationMessage For="@(() => _form.TagId)" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="tag-name">Name</label>
|
||||
<InputText id="tag-name" @bind-Value="_form.Name" class="form-control form-control-sm"
|
||||
placeholder="Temperature setpoint" />
|
||||
<ValidationMessage For="@(() => _form.Name)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="tag-driver">Driver instance</label>
|
||||
<InputSelect id="tag-driver" @bind-Value="_form.DriverInstanceId" class="form-select form-select-sm">
|
||||
<option value="">— pick a driver —</option>
|
||||
@foreach (var (id, display) in Drivers)
|
||||
{
|
||||
<option value="@id">@display</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => _form.DriverInstanceId)" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="tag-dtype">Data type</label>
|
||||
<InputSelect id="tag-dtype" @bind-Value="_form.DataType" class="form-select form-select-sm">
|
||||
@foreach (var dt in DataTypes)
|
||||
{
|
||||
<option value="@dt">@dt</option>
|
||||
}
|
||||
</InputSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="tag-access">Access level</label>
|
||||
<InputSelect id="tag-access" @bind-Value="_form.AccessLevel" class="form-select form-select-sm">
|
||||
<option value="@TagAccessLevel.Read">Read</option>
|
||||
<option value="@TagAccessLevel.ReadWrite">ReadWrite</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">WriteIdempotent</label>
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.WriteIdempotent" class="form-check-input" />
|
||||
<label class="form-check-label">Safe to retry writes (decision #44–45)</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="tag-pgroup">PollGroupId (optional)</label>
|
||||
<InputText id="tag-pgroup" @bind-Value="_form.PollGroupId" class="form-control form-control-sm mono" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="tag-config">Tag config (JSON)</label>
|
||||
<InputTextArea id="tag-config" @bind-Value="_form.TagConfig" rows="6"
|
||||
class="form-control form-control-sm mono"
|
||||
placeholder='{ "register": 40001, "scale": 0.1 }' />
|
||||
<div class="form-text">Schemaless per driver type — register / address / scaling / byte-order. Validated server-side at deploy.</div>
|
||||
<ValidationMessage For="@(() => _form.TagConfig)" />
|
||||
</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 {
|
||||
private static readonly string[] DataTypes =
|
||||
["Boolean", "SByte", "Byte", "Int16", "UInt16", "Int32", "UInt32",
|
||||
"Int64", "UInt64", "Float", "Double", "String", "DateTime", "Guid", "ByteString"];
|
||||
|
||||
/// <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 tag; <c>false</c> to edit <see cref="Existing"/>.</summary>
|
||||
[Parameter] public bool IsNew { get; set; }
|
||||
|
||||
/// <summary>The owning equipment id the created tag binds to (used only on create).</summary>
|
||||
[Parameter] public string? EquipmentId { get; set; }
|
||||
|
||||
/// <summary>The tag being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
|
||||
[Parameter] public TagEditDto? Existing { get; set; }
|
||||
|
||||
/// <summary>The candidate drivers — scoped to the equipment's cluster by the host — as <c>(Id, Display)</c> pairs.</summary>
|
||||
[Parameter] public IReadOnlyList<(string Id, string Display)> Drivers { get; set; } = Array.Empty<(string, string)>();
|
||||
|
||||
/// <summary>Raised after a successful create/save so the host can refresh the equipment's children 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;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// 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
|
||||
{
|
||||
TagId = Existing.TagId,
|
||||
Name = Existing.Name,
|
||||
DriverInstanceId = Existing.DriverInstanceId,
|
||||
DataType = Existing.DataType,
|
||||
AccessLevel = Existing.AccessLevel,
|
||||
WriteIdempotent = Existing.WriteIdempotent,
|
||||
PollGroupId = Existing.PollGroupId,
|
||||
TagConfig = Existing.TagConfig,
|
||||
};
|
||||
}
|
||||
_error = null;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_busy = true;
|
||||
_error = null;
|
||||
try
|
||||
{
|
||||
var input = new TagInput(
|
||||
_form.TagId,
|
||||
_form.Name,
|
||||
_form.DriverInstanceId,
|
||||
_form.DataType,
|
||||
_form.AccessLevel,
|
||||
_form.WriteIdempotent,
|
||||
_form.PollGroupId,
|
||||
_form.TagConfig);
|
||||
|
||||
var result = IsNew
|
||||
? await Svc.CreateTagAsync(EquipmentId!, input)
|
||||
: await Svc.UpdateTagAsync(Existing!.TagId, 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 TagId { get; set; } = "";
|
||||
[Required] public string Name { get; set; } = "";
|
||||
[Required] public string DriverInstanceId { get; set; } = "";
|
||||
public string DataType { get; set; } = "Float";
|
||||
public TagAccessLevel AccessLevel { get; set; } = TagAccessLevel.Read;
|
||||
public bool WriteIdempotent { get; set; }
|
||||
public string? PollGroupId { get; set; }
|
||||
[Required] public string TagConfig { get; set; } = "{}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
@* Create/edit modal for an equipment-bound virtual tag, wired straight into IUnsTreeService. The host
|
||||
page owns visibility and supplies the owning equipment id (create) or the loaded VirtualTagEditDto
|
||||
(edit), plus the script list. Virtual tags are always scoped to an equipment (plan decision #2) and
|
||||
that binding never moves, so this modal deliberately offers NO equipment selector — the owning
|
||||
equipment is fixed. On a successful save it raises OnSaved so the host can refresh the equipment's
|
||||
children in place. *@
|
||||
@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="virtualTagModal">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(IsNew ? "New virtual tag" : "Edit virtual tag")</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="vtag-id">VirtualTagId</label>
|
||||
<InputText id="vtag-id" @bind-Value="_form.VirtualTagId" disabled="@(!IsNew)"
|
||||
class="form-control form-control-sm mono" />
|
||||
<ValidationMessage For="@(() => _form.VirtualTagId)" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="vtag-name">Name</label>
|
||||
<InputText id="vtag-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="vtag-dtype">DataType</label>
|
||||
<InputText id="vtag-dtype" @bind-Value="_form.DataType" class="form-control form-control-sm mono"
|
||||
placeholder="Double" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="vtag-script">Script</label>
|
||||
<InputSelect id="vtag-script" @bind-Value="_form.ScriptId" 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.ScriptId)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Change-triggered</label>
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.ChangeTriggered" class="form-check-input" />
|
||||
<label class="form-check-label">Re-evaluate on dependency change</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label" for="vtag-timer">TimerIntervalMs (optional)</label>
|
||||
<InputNumber id="vtag-timer" @bind-Value="_form.TimerIntervalMs" class="form-control form-control-sm" />
|
||||
<div class="form-text">Periodic re-evaluation. Null = change-trigger only.</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Historize</label>
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.Historize" class="form-check-input" />
|
||||
<label class="form-check-label">Send to Wonderware historian</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="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 virtual tag in deployments</label>
|
||||
</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 virtual tag; <c>false</c> to edit <see cref="Existing"/>.</summary>
|
||||
[Parameter] public bool IsNew { get; set; }
|
||||
|
||||
/// <summary>The owning equipment id the created virtual tag binds to (used only on create).</summary>
|
||||
[Parameter] public string? EquipmentId { get; set; }
|
||||
|
||||
/// <summary>The virtual tag being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
|
||||
[Parameter] public VirtualTagEditDto? Existing { get; set; }
|
||||
|
||||
/// <summary>The selectable 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 children 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;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// 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
|
||||
{
|
||||
VirtualTagId = Existing.VirtualTagId,
|
||||
Name = Existing.Name,
|
||||
DataType = Existing.DataType,
|
||||
ScriptId = Existing.ScriptId,
|
||||
ChangeTriggered = Existing.ChangeTriggered,
|
||||
TimerIntervalMs = Existing.TimerIntervalMs,
|
||||
Historize = Existing.Historize,
|
||||
Enabled = Existing.Enabled,
|
||||
};
|
||||
}
|
||||
_error = null;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_busy = true;
|
||||
_error = null;
|
||||
try
|
||||
{
|
||||
var input = new VirtualTagInput(
|
||||
_form.VirtualTagId,
|
||||
_form.Name,
|
||||
_form.DataType,
|
||||
_form.ScriptId,
|
||||
_form.ChangeTriggered,
|
||||
_form.TimerIntervalMs,
|
||||
_form.Historize,
|
||||
_form.Enabled);
|
||||
|
||||
var result = IsNew
|
||||
? await Svc.CreateVirtualTagAsync(EquipmentId!, input)
|
||||
: await Svc.UpdateVirtualTagAsync(Existing!.VirtualTagId, 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 VirtualTagId { get; set; } = "";
|
||||
[Required] public string Name { get; set; } = "";
|
||||
public string DataType { get; set; } = "Double";
|
||||
[Required] public string ScriptId { get; set; } = "";
|
||||
public bool ChangeTriggered { get; set; } = true;
|
||||
public int? TimerIntervalMs { get; set; }
|
||||
public bool Historize { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user