feat(adminui): relay->alias converter UI (per-equipment + fleet-wide /uns/convert-relays)
This commit is contained in:
@@ -16,6 +16,9 @@
|
|||||||
<NavRailItem Href="/hosts" Text="Host status" />
|
<NavRailItem Href="/hosts" Text="Host status" />
|
||||||
<NavRailItem Href="/clusters" Text="Clusters" />
|
<NavRailItem Href="/clusters" Text="Clusters" />
|
||||||
<NavRailItem Href="/uns" Text="UNS" />
|
<NavRailItem Href="/uns" Text="UNS" />
|
||||||
|
<AuthorizeView Policy="FleetAdmin">
|
||||||
|
<NavRailItem Href="/uns/convert-relays" Text="Convert relays" />
|
||||||
|
</AuthorizeView>
|
||||||
<NavRailItem Href="/reservations" Text="Reservations" />
|
<NavRailItem Href="/reservations" Text="Reservations" />
|
||||||
<NavRailItem Href="/certificates" Text="Certificates" />
|
<NavRailItem Href="/certificates" Text="Certificates" />
|
||||||
<NavRailItem Href="/role-grants" Text="Role grants" />
|
<NavRailItem Href="/role-grants" Text="Role grants" />
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ else
|
|||||||
}
|
}
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddAlias" disabled="@(_gateways.Count == 0)">Add alias (browse Galaxy)</button>
|
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddAlias" disabled="@(_gateways.Count == 0)">Add alias (browse Galaxy)</button>
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddTag">Add tag</button>
|
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="OpenAddTag">Add tag</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="PreviewConvertRelaysAsync" disabled="@(_gateways.Count == 0 || _convertBusy)">Convert relay virtual-tags…</button>
|
||||||
</div>
|
</div>
|
||||||
@if (!string.IsNullOrWhiteSpace(_tagError))
|
@if (!string.IsNullOrWhiteSpace(_tagError))
|
||||||
{
|
{
|
||||||
@@ -196,6 +197,76 @@ else
|
|||||||
</table>
|
</table>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_convertMessage))
|
||||||
|
{
|
||||||
|
<div class="text-success small mb-2">@_convertMessage</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_convertPreview is not null)
|
||||||
|
{
|
||||||
|
<section class="panel rise mt-2">
|
||||||
|
<div class="panel-head d-flex justify-content-between align-items-center">
|
||||||
|
<span>Convert relay virtual-tags to aliases</span>
|
||||||
|
<button type="button" class="btn btn-sm btn-link" @onclick="@(() => { _convertPreview = null; })">Close</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding:1rem">
|
||||||
|
<h6 class="text-muted">Will convert (@_convertPreview.Converted.Count)</h6>
|
||||||
|
@if (_convertPreview.Converted.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="text-muted small mb-3">No relay virtual-tags to convert.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table table-sm mb-3">
|
||||||
|
<thead><tr><th>Virtual tag</th><th>Full name</th><th>Data type</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var c in _convertPreview.Converted)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@c.VirtualTagName</td>
|
||||||
|
<td class="mono">@c.FullName</td>
|
||||||
|
<td>@c.DataType</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h6 class="text-muted">Skipped (@_convertPreview.Skipped.Count)</h6>
|
||||||
|
@if (_convertPreview.Skipped.Count == 0)
|
||||||
|
{
|
||||||
|
<p class="text-muted small mb-0">Nothing skipped.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<thead><tr><th>Virtual tag</th><th>Reason</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var s in _convertPreview.Skipped)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@s.VirtualTagName</td>
|
||||||
|
<td>@s.Reason</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mt-3 d-flex gap-2">
|
||||||
|
@if (_convertPreview.Converted.Count > 0)
|
||||||
|
{
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" @onclick="ApplyConvertRelaysAsync" disabled="@_convertBusy">
|
||||||
|
@if (_convertBusy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="@(() => { _convertPreview = null; })" disabled="@_convertBusy">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
<TagModal Visible="_tagModalVisible" IsNew="_tagModalIsNew" EquipmentId="@EquipmentId"
|
<TagModal Visible="_tagModalVisible" IsNew="_tagModalIsNew" EquipmentId="@EquipmentId"
|
||||||
Existing="_tagModalExisting" Drivers="_tagDriverOptions"
|
Existing="_tagModalExisting" Drivers="_tagDriverOptions"
|
||||||
OnSaved="OnTagSavedAsync" OnCancel="@(() => { _tagModalVisible = false; })" />
|
OnSaved="OnTagSavedAsync" OnCancel="@(() => { _tagModalVisible = false; })" />
|
||||||
@@ -331,6 +402,12 @@ else
|
|||||||
private bool _aliasModalIsNew;
|
private bool _aliasModalIsNew;
|
||||||
private AliasTagEditDto? _aliasModalExisting;
|
private AliasTagEditDto? _aliasModalExisting;
|
||||||
|
|
||||||
|
// --- Relay→alias converter (per-equipment). _convertPreview holds the dry-run result while the
|
||||||
|
// inline preview panel is open; null = panel closed. _convertMessage is a brief post-apply summary. ---
|
||||||
|
private RelayConversionResult? _convertPreview;
|
||||||
|
private bool _convertBusy;
|
||||||
|
private string? _convertMessage;
|
||||||
|
|
||||||
// --- Virtual Tags tab state. _vtags is null until the tab is first activated. ---
|
// --- Virtual Tags tab state. _vtags is null until the tab is first activated. ---
|
||||||
private IReadOnlyList<EquipmentVirtualTagRow>? _vtags;
|
private IReadOnlyList<EquipmentVirtualTagRow>? _vtags;
|
||||||
private string? _vtagError;
|
private string? _vtagError;
|
||||||
@@ -440,6 +517,36 @@ else
|
|||||||
await ReloadTagsAsync();
|
await ReloadTagsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Relay→alias converter handlers (per-equipment, scoped to EquipmentId). Dry-run first to fill
|
||||||
|
// the inline preview panel; Apply mutates the draft then reloads the tags + virtual-tags lists so
|
||||||
|
// the converted vtags vanish and the new aliases appear. ---
|
||||||
|
|
||||||
|
private async Task PreviewConvertRelaysAsync()
|
||||||
|
{
|
||||||
|
_convertMessage = null;
|
||||||
|
_convertBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_convertPreview = await Svc.ConvertRelayVirtualTagsToAliasesAsync(EquipmentId!, dryRun: true);
|
||||||
|
}
|
||||||
|
finally { _convertBusy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyConvertRelaysAsync()
|
||||||
|
{
|
||||||
|
_convertBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var r = await Svc.ConvertRelayVirtualTagsToAliasesAsync(EquipmentId!, dryRun: false);
|
||||||
|
_convertPreview = null;
|
||||||
|
_convertMessage = $"Converted {r.Converted.Count}, skipped {r.Skipped.Count}.";
|
||||||
|
// The converted virtual tags become aliases, so both lists change.
|
||||||
|
await ReloadTagsAsync();
|
||||||
|
await ReloadVirtualTagsAsync();
|
||||||
|
}
|
||||||
|
finally { _convertBusy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
// --- Virtual Tags tab handlers ---
|
// --- Virtual Tags tab handlers ---
|
||||||
|
|
||||||
private async Task ReloadVirtualTagsAsync()
|
private async Task ReloadVirtualTagsAsync()
|
||||||
@@ -542,6 +649,9 @@ else
|
|||||||
_tags = null;
|
_tags = null;
|
||||||
_vtags = null;
|
_vtags = null;
|
||||||
_alarms = null;
|
_alarms = null;
|
||||||
|
// Drop any open relay-conversion preview/summary so it can't leak across equipment changes.
|
||||||
|
_convertPreview = null;
|
||||||
|
_convertMessage = null;
|
||||||
if (!IsNew)
|
if (!IsNew)
|
||||||
{
|
{
|
||||||
_equipment = await Svc.LoadEquipmentAsync(EquipmentId!);
|
_equipment = await Svc.LoadEquipmentAsync(EquipmentId!);
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
@page "/uns/convert-relays"
|
||||||
|
@* Fleet-wide relay→alias converter. Previews (dry-run) and applies the conversion of pure-relay
|
||||||
|
virtual tags into Galaxy alias tags across every equipment. Edits the draft config only; the
|
||||||
|
operator publishes normally afterward. FleetAdmin-gated (mirrors RoleGrants.razor). *@
|
||||||
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "FleetAdmin")]
|
||||||
|
@rendermode RenderMode.InteractiveServer
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||||
|
@inject IUnsTreeService Svc
|
||||||
|
|
||||||
|
<PageTitle>Convert relays</PageTitle>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Convert relay virtual-tags to aliases</h4>
|
||||||
|
<a href="/uns" class="btn btn-outline-secondary btn-sm">Back to UNS</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
Sweeps every equipment for pure-relay virtual tags (a script body that simply returns another
|
||||||
|
tag's value) and rewrites each one as a Galaxy alias tag. This edits the draft configuration
|
||||||
|
only — publish afterward to take effect. Run a preview first to review what will change.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2 align-items-center mb-3">
|
||||||
|
<button type="button" class="btn btn-primary" @onclick="PreviewAsync" disabled="@_busy">
|
||||||
|
@if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
@if (_applied)
|
||||||
|
{
|
||||||
|
<span class="text-success small">Conversion applied. Publish to take effect.</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_error))
|
||||||
|
{
|
||||||
|
<div class="text-danger small mb-3">@_error</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (_preview is not null)
|
||||||
|
{
|
||||||
|
<section class="panel rise mt-2">
|
||||||
|
<div class="panel-head">Will convert (@_preview.Converted.Count)</div>
|
||||||
|
@if (_preview.Converted.Count == 0)
|
||||||
|
{
|
||||||
|
<div style="padding:1rem" class="text-muted">No relay virtual-tags to convert.</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead><tr><th>Equipment</th><th>Virtual tag</th><th>Full name</th><th>Data type</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var c in _preview.Converted)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="mono">@c.EquipmentId</td>
|
||||||
|
<td>@c.VirtualTagName</td>
|
||||||
|
<td class="mono">@c.FullName</td>
|
||||||
|
<td>@c.DataType</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel rise mt-3">
|
||||||
|
<div class="panel-head">Skipped (@_preview.Skipped.Count)</div>
|
||||||
|
@if (_preview.Skipped.Count == 0)
|
||||||
|
{
|
||||||
|
<div style="padding:1rem" class="text-muted">Nothing skipped.</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead><tr><th>Equipment</th><th>Virtual tag</th><th>Reason</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var s in _preview.Skipped)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="mono">@s.EquipmentId</td>
|
||||||
|
<td>@s.VirtualTagName</td>
|
||||||
|
<td>@s.Reason</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@if (!_preview.Applied && _preview.Converted.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="d-flex gap-2 align-items-center mt-3">
|
||||||
|
@if (!_confirming)
|
||||||
|
{
|
||||||
|
<button type="button" class="btn btn-warning" @onclick="() => _confirming = true" disabled="@_busy">
|
||||||
|
Apply conversion…
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="small">Convert @_preview.Converted.Count relay virtual-tag(s) to aliases in the draft?</span>
|
||||||
|
<button type="button" class="btn btn-danger btn-sm" @onclick="ApplyAsync" disabled="@_busy">
|
||||||
|
@if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||||
|
Yes, apply
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="() => _confirming = false" disabled="@_busy">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private RelayConversionResult? _preview;
|
||||||
|
private bool _busy;
|
||||||
|
private bool _confirming;
|
||||||
|
private bool _applied;
|
||||||
|
private string? _error;
|
||||||
|
|
||||||
|
private async Task PreviewAsync()
|
||||||
|
{
|
||||||
|
_error = null;
|
||||||
|
_confirming = false;
|
||||||
|
_applied = false;
|
||||||
|
_busy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_preview = await Svc.ConvertRelayVirtualTagsToAliasesAsync(null, dryRun: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { _error = ex.Message; }
|
||||||
|
finally { _busy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyAsync()
|
||||||
|
{
|
||||||
|
_error = null;
|
||||||
|
_busy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Svc.ConvertRelayVirtualTagsToAliasesAsync(null, dryRun: false);
|
||||||
|
_applied = true;
|
||||||
|
_confirming = false;
|
||||||
|
// Re-run the preview so the Converted set is now empty and only remaining skips show.
|
||||||
|
_preview = await Svc.ConvertRelayVirtualTagsToAliasesAsync(null, dryRun: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { _error = ex.Message; }
|
||||||
|
finally { _busy = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user