using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns; /// /// Default . Reads the structural rows with a handful of /// untracked queries, computes per-equipment tag/virtual-tag counts, and hands the flat /// rows to the pure to nest into the browse tree. /// /// /// Each call creates and disposes its own context via the pooled factory — the same pattern /// every AdminUI page uses — so the service is safe to register as Scoped and used per /// Blazor circuit. /// public sealed class UnsTreeService(IDbContextFactory dbFactory) : IUnsTreeService { /// public async Task> LoadStructureAsync(CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); var clusters = await db.ServerClusters .AsNoTracking() .Select(c => new ClusterRow(c.ClusterId, c.Enterprise, c.Site, c.Name)) .ToListAsync(ct); var areas = await db.UnsAreas .AsNoTracking() .Select(a => new AreaRow(a.UnsAreaId, a.ClusterId, a.Name)) .ToListAsync(ct); var lines = await db.UnsLines .AsNoTracking() .Select(l => new LineRow(l.UnsLineId, l.UnsAreaId, l.Name)) .ToListAsync(ct); var equipmentRows = await db.Equipment .AsNoTracking() .Select(e => new { e.EquipmentId, e.UnsLineId, e.MachineCode, e.Name, }) .ToListAsync(ct); // Per-equipment driver-tag counts (tags with no equipment are excluded). var tagCounts = (await db.Tags .AsNoTracking() .Where(t => t.EquipmentId != null) .GroupBy(t => t.EquipmentId) .Select(g => new { EquipmentId = g.Key!, Count = g.Count() }) .ToListAsync(ct)) .ToDictionary(x => x.EquipmentId, x => x.Count, StringComparer.Ordinal); // Per-equipment virtual-tag counts (virtual tags with no equipment are excluded). var vtagCounts = (await db.VirtualTags .AsNoTracking() .Where(v => v.EquipmentId != null) .GroupBy(v => v.EquipmentId) .Select(g => new { EquipmentId = g.Key!, Count = g.Count() }) .ToListAsync(ct)) .ToDictionary(x => x.EquipmentId, x => x.Count, StringComparer.Ordinal); var equipment = equipmentRows .Select(e => new EquipmentRow( e.EquipmentId, e.UnsLineId, e.MachineCode, e.Name, tagCounts.GetValueOrDefault(e.EquipmentId), vtagCounts.GetValueOrDefault(e.EquipmentId))) .ToList(); return UnsTreeAssembly.Build(clusters, areas, lines, equipment); } /// public async Task> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); // Left-join each tag to its driver so we can tell Galaxy aliases apart while still surfacing a // tag whose driver row is missing (it is simply treated as a non-alias). EF can't parse the // TagConfig JSON in-query, so we materialise then map IsAlias/Source in memory. var rows = await db.Tags.AsNoTracking() .Where(t => t.EquipmentId == equipmentId) .OrderBy(t => t.Name) .GroupJoin(db.DriverInstances.AsNoTracking(), t => t.DriverInstanceId, d => d.DriverInstanceId, (t, ds) => new { Tag = t, Drivers = ds }) .SelectMany(x => x.Drivers.DefaultIfEmpty(), (x, d) => new { x.Tag.TagId, x.Tag.Name, x.Tag.DriverInstanceId, x.Tag.DataType, x.Tag.AccessLevel, DriverType = d != null ? d.DriverType : null, x.Tag.TagConfig, }) .ToListAsync(ct); return rows.Select(r => { var isAlias = r.DriverType == "GalaxyMxGateway"; var fullName = isAlias ? ExtractTagConfigFullName(r.TagConfig) : null; var source = fullName is not null ? $"galaxy:{fullName}" : null; return new EquipmentTagRow(r.TagId, r.Name, r.DriverInstanceId, r.DataType, r.AccessLevel, isAlias, source); }).ToList(); } /// public async Task> LoadVirtualTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.VirtualTags.AsNoTracking() .Where(v => v.EquipmentId == equipmentId) .OrderBy(v => v.Name) .Select(v => new EquipmentVirtualTagRow(v.VirtualTagId, v.Name, v.DataType, v.ScriptId, v.Enabled)) .ToListAsync(ct); } /// public async Task LoadAreaAsync(string unsAreaId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.UnsAreas .AsNoTracking() .Where(a => a.UnsAreaId == unsAreaId) .Select(a => new AreaEditDto(a.UnsAreaId, a.Name, a.Notes, a.ClusterId, a.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task LoadLineAsync(string unsLineId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.UnsLines .AsNoTracking() .Where(l => l.UnsLineId == unsLineId) .Select(l => new LineEditDto(l.UnsLineId, l.UnsAreaId, l.Name, l.Notes, l.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task LoadEquipmentAsync(string equipmentId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.Equipment .AsNoTracking() .Where(e => e.EquipmentId == equipmentId) .Select(e => new EquipmentEditDto( e.EquipmentId, e.Name, e.MachineCode, e.UnsLineId, e.DriverInstanceId, e.ZTag, e.SAPID, e.Manufacturer, e.Model, e.SerialNumber, e.HardwareRevision, e.SoftwareRevision, e.YearOfConstruction, e.AssetLocation, e.ManufacturerUri, e.DeviceManualUri, e.Enabled, e.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task LoadTagAsync(string tagId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.Tags .AsNoTracking() .Where(t => t.TagId == tagId) .Select(t => new TagEditDto( t.TagId, t.EquipmentId!, t.Name, t.DriverInstanceId, t.DataType, t.AccessLevel, t.WriteIdempotent, t.PollGroupId, t.TagConfig, t.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task LoadVirtualTagAsync(string virtualTagId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); return await db.VirtualTags .AsNoTracking() .Where(v => v.VirtualTagId == virtualTagId) .Select(v => new VirtualTagEditDto( v.VirtualTagId, v.EquipmentId, v.Name, v.DataType, v.ScriptId, v.ChangeTriggered, v.TimerIntervalMs, v.Historize, v.Enabled, v.RowVersion)) .FirstOrDefaultAsync(ct); } /// public async Task> LoadDriversForClusterAsync( string clusterId, CancellationToken ct = default) { await using var db = await dbFactory.CreateDbContextAsync(ct); var drivers = await db.DriverInstances .AsNoTracking() .Where(d => d.ClusterId == clusterId) .OrderBy(d => d.DriverInstanceId) .Select(d => new { d.DriverInstanceId, d.Name, d.DriverType }) .ToListAsync(ct); return drivers .Select(d => (d.DriverInstanceId, Display: $"{d.DriverInstanceId} — {d.Name} ({d.DriverType})")) .ToList(); } /// public async Task LoadEquipmentPickContextAsync(string? lineId, CancellationToken ct = default) { var empty = new EquipmentPickContext(Array.Empty<(string, string)>(), Array.Empty<(string, string)>()); if (string.IsNullOrEmpty(lineId)) return empty; await using var db = await dbFactory.CreateDbContextAsync(ct); // line -> area -> clusterId var clusterId = await (from l in db.UnsLines.AsNoTracking() join a in db.UnsAreas.AsNoTracking() on l.UnsAreaId equals a.UnsAreaId where l.UnsLineId == lineId select a.ClusterId).FirstOrDefaultAsync(ct); if (string.IsNullOrEmpty(clusterId)) return empty; // all lines in that cluster (for the line