feat(uns): wire Tags + Virtual Tags tabs on the equipment page (reuse modals)
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns
|
||||
@inject IUnsTreeService Svc
|
||||
@inject NavigationManager Nav
|
||||
|
||||
@@ -31,8 +32,8 @@ else
|
||||
{
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("details")" @onclick='() => _activeTab = "details"'>Details</button></li>
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("tags")" @onclick='() => _activeTab = "tags"' disabled="@IsNew">Tags</button></li>
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("vtags")" @onclick='() => _activeTab = "vtags"' disabled="@IsNew">Virtual Tags</button></li>
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("tags")" @onclick='() => ShowTabAsync("tags")' disabled="@IsNew">Tags</button></li>
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("vtags")" @onclick='() => ShowTabAsync("vtags")' disabled="@IsNew">Virtual Tags</button></li>
|
||||
<li class="nav-item"><button type="button" class="nav-link @TabClass("alarms")" @onclick='() => _activeTab = "alarms"' disabled="@IsNew">Alarms</button></li>
|
||||
</ul>
|
||||
|
||||
@@ -135,8 +136,96 @@ else
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
else if (_activeTab == "tags") { <p class="text-muted">Tags tab — wired in a later task.</p> }
|
||||
else if (_activeTab == "vtags") { <p class="text-muted">Virtual Tags tab — wired in a later task.</p> }
|
||||
else if (_activeTab == "tags")
|
||||
{
|
||||
<div class="d-flex justify-content-end mb-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddTag">Add tag</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_tagError))
|
||||
{
|
||||
<div class="text-danger small mb-2">@_tagError</div>
|
||||
}
|
||||
@if (_tags is null)
|
||||
{
|
||||
<p class="text-muted"><span class="spinner-border spinner-border-sm me-1"></span>Loading…</p>
|
||||
}
|
||||
else if (_tags.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No tags yet.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Driver</th><th>Data type</th><th>Access</th><th class="text-end">Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var t in _tags)
|
||||
{
|
||||
<tr>
|
||||
<td>@t.Name</td>
|
||||
<td class="mono">@t.DriverInstanceId</td>
|
||||
<td>@t.DataType</td>
|
||||
<td>@t.AccessLevel</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm me-1" @onclick="() => OpenEditTag(t.TagId)">Edit</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" @onclick="() => DeleteTag(t.TagId)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
<TagModal Visible="_tagModalVisible" IsNew="_tagModalIsNew" EquipmentId="@EquipmentId"
|
||||
Existing="_tagModalExisting" Drivers="_tagDriverOptions"
|
||||
OnSaved="OnTagSavedAsync" OnCancel="@(() => { _tagModalVisible = false; })" />
|
||||
}
|
||||
else if (_activeTab == "vtags")
|
||||
{
|
||||
<div class="d-flex justify-content-end mb-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddVirtualTag">Add virtual tag</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_vtagError))
|
||||
{
|
||||
<div class="text-danger small mb-2">@_vtagError</div>
|
||||
}
|
||||
@if (_vtags is null)
|
||||
{
|
||||
<p class="text-muted"><span class="spinner-border spinner-border-sm me-1"></span>Loading…</p>
|
||||
}
|
||||
else if (_vtags.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No virtual tags yet.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Data type</th><th>Script</th><th>Enabled</th><th class="text-end">Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var v in _vtags)
|
||||
{
|
||||
<tr>
|
||||
<td>@v.Name</td>
|
||||
<td>@v.DataType</td>
|
||||
<td class="mono">@v.ScriptId</td>
|
||||
<td>@(v.Enabled ? "Yes" : "No")</td>
|
||||
<td class="text-end">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm me-1" @onclick="() => OpenEditVirtualTag(v.VirtualTagId)">Edit</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" @onclick="() => DeleteVirtualTag(v.VirtualTagId)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
<VirtualTagModal Visible="_vtagModalVisible" IsNew="_vtagModalIsNew" EquipmentId="@EquipmentId"
|
||||
Existing="_vtagModalExisting" Scripts="_vtagScriptOptions"
|
||||
OnSaved="OnVirtualTagSavedAsync" OnCancel="@(() => { _vtagModalVisible = false; })" />
|
||||
}
|
||||
else if (_activeTab == "alarms") { <p class="text-muted">Alarms tab — wired in a later task.</p> }
|
||||
}
|
||||
|
||||
@@ -158,12 +247,134 @@ else
|
||||
private IReadOnlyList<(string Id, string Display)> _lineOptions = Array.Empty<(string, string)>();
|
||||
private IReadOnlyList<(string Id, string Display)> _driverOptions = Array.Empty<(string, string)>();
|
||||
|
||||
// --- Tags tab state. _tags is null until the tab is first activated (drives the lazy load + spinner). ---
|
||||
private IReadOnlyList<EquipmentTagRow>? _tags;
|
||||
private string? _tagError;
|
||||
private bool _tagModalVisible;
|
||||
private bool _tagModalIsNew;
|
||||
private TagEditDto? _tagModalExisting;
|
||||
private IReadOnlyList<(string Id, string Display, string DriverType)> _tagDriverOptions = Array.Empty<(string, string, string)>();
|
||||
|
||||
// --- Virtual Tags tab state. _vtags is null until the tab is first activated. ---
|
||||
private IReadOnlyList<EquipmentVirtualTagRow>? _vtags;
|
||||
private string? _vtagError;
|
||||
private bool _vtagModalVisible;
|
||||
private bool _vtagModalIsNew;
|
||||
private VirtualTagEditDto? _vtagModalExisting;
|
||||
private IReadOnlyList<(string Id, string Display)> _vtagScriptOptions = Array.Empty<(string, string)>();
|
||||
|
||||
private string TabClass(string tab) => _activeTab == tab ? "active" : "";
|
||||
|
||||
/// <summary>
|
||||
/// Switches to a tab, lazily loading its list on first activation. The Tags/Virtual Tags lists are
|
||||
/// null until first shown (and are reset to null in OnParametersSetAsync when the equipment changes),
|
||||
/// so the fetch runs once per equipment rather than on every render. Equipment is fixed to
|
||||
/// <see cref="EquipmentId"/>; the tabs are disabled while IsNew so this is only ever reached with a
|
||||
/// persisted equipment.
|
||||
/// </summary>
|
||||
private async Task ShowTabAsync(string tab)
|
||||
{
|
||||
_activeTab = tab;
|
||||
if (IsNew) { return; }
|
||||
if (tab == "tags" && _tags is null) { await ReloadTagsAsync(); }
|
||||
else if (tab == "vtags" && _vtags is null) { await ReloadVirtualTagsAsync(); }
|
||||
}
|
||||
|
||||
// --- Tags tab handlers (mirror GlobalUns; the owning equipment is fixed = EquipmentId) ---
|
||||
|
||||
private async Task ReloadTagsAsync()
|
||||
{
|
||||
_tags = await Svc.LoadTagsForEquipmentAsync(EquipmentId!);
|
||||
}
|
||||
|
||||
private async Task OpenAddTag()
|
||||
{
|
||||
_tagModalIsNew = true;
|
||||
_tagModalExisting = null;
|
||||
_tagDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(EquipmentId!);
|
||||
_tagModalVisible = true;
|
||||
}
|
||||
|
||||
private async Task OpenEditTag(string tagId)
|
||||
{
|
||||
var dto = await Svc.LoadTagAsync(tagId);
|
||||
if (dto is null) { return; }
|
||||
_tagModalIsNew = false;
|
||||
_tagModalExisting = dto;
|
||||
_tagDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(EquipmentId!);
|
||||
_tagModalVisible = true;
|
||||
}
|
||||
|
||||
private async Task OnTagSavedAsync()
|
||||
{
|
||||
_tagModalVisible = false;
|
||||
await ReloadTagsAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteTag(string tagId)
|
||||
{
|
||||
_tagError = null;
|
||||
// Load the tag fresh to capture its current RowVersion for the optimistic-concurrency delete.
|
||||
var dto = await Svc.LoadTagAsync(tagId);
|
||||
if (dto is null) { await ReloadTagsAsync(); return; }
|
||||
var r = await Svc.DeleteTagAsync(tagId, dto.RowVersion);
|
||||
if (r.Ok) { await ReloadTagsAsync(); }
|
||||
else { _tagError = r.Error; }
|
||||
}
|
||||
|
||||
// --- Virtual Tags tab handlers ---
|
||||
|
||||
private async Task ReloadVirtualTagsAsync()
|
||||
{
|
||||
_vtags = await Svc.LoadVirtualTagsForEquipmentAsync(EquipmentId!);
|
||||
}
|
||||
|
||||
private async Task OpenAddVirtualTag()
|
||||
{
|
||||
_vtagModalIsNew = true;
|
||||
_vtagModalExisting = null;
|
||||
_vtagScriptOptions = await Svc.LoadScriptsAsync();
|
||||
_vtagModalVisible = true;
|
||||
}
|
||||
|
||||
private async Task OpenEditVirtualTag(string vtagId)
|
||||
{
|
||||
var dto = await Svc.LoadVirtualTagAsync(vtagId);
|
||||
if (dto is null) { return; }
|
||||
_vtagModalIsNew = false;
|
||||
_vtagModalExisting = dto;
|
||||
_vtagScriptOptions = await Svc.LoadScriptsAsync();
|
||||
_vtagModalVisible = true;
|
||||
}
|
||||
|
||||
private async Task OnVirtualTagSavedAsync()
|
||||
{
|
||||
_vtagModalVisible = false;
|
||||
await ReloadVirtualTagsAsync();
|
||||
}
|
||||
|
||||
private async Task DeleteVirtualTag(string vtagId)
|
||||
{
|
||||
_vtagError = null;
|
||||
// Load the virtual tag fresh to capture its current RowVersion for the concurrency-guarded delete.
|
||||
var dto = await Svc.LoadVirtualTagAsync(vtagId);
|
||||
if (dto is null) { await ReloadVirtualTagsAsync(); return; }
|
||||
var r = await Svc.DeleteVirtualTagAsync(vtagId, dto.RowVersion);
|
||||
if (r.Ok) { await ReloadVirtualTagsAsync(); }
|
||||
else { _vtagError = r.Error; }
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
_loading = true;
|
||||
_error = null;
|
||||
// _activeTab is intentionally NOT reset here: an in-place save reloads the page (re-runs
|
||||
// OnParametersSetAsync) and the user's tab selection should survive that. The create→redirect
|
||||
// path lands on Details because the field initializes to "details" and a fresh page instance
|
||||
// starts with that initial value. The Tags/Virtual Tags lists are reset to null below so the
|
||||
// lazy loaders re-fetch for the (possibly different) equipment this parameter set targets.
|
||||
_tags = null;
|
||||
_vtags = null;
|
||||
if (!IsNew)
|
||||
{
|
||||
_equipment = await Svc.LoadEquipmentAsync(EquipmentId!);
|
||||
|
||||
Reference in New Issue
Block a user