Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/FleetDiagnosticsLinks.cs
T
Joseph Doherty fc3178cca8 feat(adminui): link /fleet rows to node diagnostics; show authored driver names (A-F4, A-F5)
/fleet/diagnostics was reachable only by URL: each driver-role row on /fleet now
links to it (FleetDiagnosticsLinks.For escapes the host:port node id). The
diagnostics table showed the string instance id because the wire carries no
display name; the page now joins the authored DriverInstances.Name from the
ConfigDb (enrichment only - a failed lookup falls back to the id) and keeps the
id beneath the name.

Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 17:59:32 -04:00

47 lines
2.3 KiB
C#

using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Clients;
/// <summary>
/// Helpers for the per-node diagnostics page (A-F4 / A-F5). <c>/fleet/diagnostics</c> was reachable only
/// by URL; <c>/fleet</c> now links each driver node's row to it. The page also showed the driver's
/// string instance id where the operator expects its authored name — the diagnostics wire carries only
/// the id (<c>DriverInstanceDiagnostics.Name</c>), so the AdminUI joins the name from the ConfigDb.
/// </summary>
public static class FleetDiagnosticsLinks
{
/// <summary>The diagnostics page route.</summary>
public const string PagePath = "/fleet/diagnostics";
/// <summary>The diagnostics URL for one node.</summary>
/// <param name="nodeId">The node id as <c>/fleet</c> shows it (<c>host:port</c>).</param>
/// <returns>A relative URL with the node escaped.</returns>
public static string For(string nodeId) => PagePath + "?node=" + Uri.EscapeDataString(nodeId);
/// <summary>Loads the authored display names of the given driver instance ids.</summary>
/// <param name="db">The config DB context.</param>
/// <param name="driverInstanceIds">The string driver instance ids from the snapshot.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Id → authored name (ordinal-ignore-case keys); ids with no configured row are absent.</returns>
public static async Task<IReadOnlyDictionary<string, string>> LoadDisplayNamesAsync(
OtOpcUaConfigDbContext db, IReadOnlyCollection<string> driverInstanceIds, CancellationToken ct = default)
{
if (driverInstanceIds.Count == 0) return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var ids = driverInstanceIds.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
var rows = await db.DriverInstances.AsNoTracking()
.Where(d => ids.Contains(d.DriverInstanceId))
.Select(d => new { d.DriverInstanceId, d.Name })
.ToListAsync(ct);
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var r in rows)
{
if (!string.IsNullOrWhiteSpace(r.Name)) map[r.DriverInstanceId] = r.Name;
}
return map;
}
}