diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterEquipment.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterEquipment.razor deleted file mode 100644 index 2a195572..00000000 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterEquipment.razor +++ /dev/null @@ -1,99 +0,0 @@ -@page "/clusters/{ClusterId}/equipment" -@attribute [Microsoft.AspNetCore.Authorization.Authorize] -@rendermode RenderMode.InteractiveServer -@using Microsoft.EntityFrameworkCore -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@inject IDbContextFactory DbFactory - -
-

Equipment · @ClusterId

- -
- - - -@if (_rows is null) -{ -

Loading…

-} -else -{ -
- Equipment rows are scoped to a UNS line and optionally bound to a driver instance - (driver-less = VirtualTag-only). EquipmentId is - system-generated (decision #125); browse identifiers are MachineCode (operator) + ZTag - (ERP). -
- -
-
@_rows.Count equipment row@(_rows.Count == 1 ? "" : "s")
- @if (_rows.Count == 0) - { -
No equipment defined for this cluster.
- } - else - { -
- - - - - - - - - - - - - - - @foreach (var e in _rows) - { - - - - - - - - - - - } - -
EquipmentIdNameMachineCodeZTagDriverUNS lineIdentification
@e.EquipmentId@e.Name@e.MachineCode@(e.ZTag ?? "—")@e.DriverInstanceId@e.UnsLineId - @if (!string.IsNullOrWhiteSpace(e.Manufacturer)) { @e.Manufacturer } - @if (!string.IsNullOrWhiteSpace(e.Model)) { / @e.Model } - Edit
-
- } -
-} - -@code { - [Parameter] public string ClusterId { get; set; } = ""; - private List? _rows; - - protected override async Task OnInitializedAsync() - { - await using var db = await DbFactory.CreateDbContextAsync(); - var driversInCluster = db.DriverInstances.AsNoTracking() - .Where(d => d.ClusterId == ClusterId).Select(d => d.DriverInstanceId); - // Driver-less equipment (DriverInstanceId == null) has no DriverInstance FK. - // Scope it to this cluster via UnsLine → UnsArea.ClusterId instead. - var areaIds = db.UnsAreas.AsNoTracking() - .Where(a => a.ClusterId == ClusterId).Select(a => a.UnsAreaId); - var linesInCluster = db.UnsLines.AsNoTracking() - .Where(l => areaIds.Contains(l.UnsAreaId)).Select(l => l.UnsLineId); - _rows = await db.Equipment.AsNoTracking() - .Where(e => driversInCluster.Contains(e.DriverInstanceId) - || (e.DriverInstanceId == null && linesInCluster.Contains(e.UnsLineId))) - .OrderBy(e => e.Name) - .ToListAsync(); - } -} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterTags.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterTags.razor deleted file mode 100644 index 61a57a49..00000000 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterTags.razor +++ /dev/null @@ -1,106 +0,0 @@ -@page "/clusters/{ClusterId}/tags" -@attribute [Microsoft.AspNetCore.Authorization.Authorize] -@rendermode RenderMode.InteractiveServer -@using Microsoft.EntityFrameworkCore -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@inject IDbContextFactory DbFactory - -
-

Tags · @ClusterId

- New tag -
- - - -@if (_rows is null) -{ -

Loading…

-} -else -{ -
- Tags are bound to a driver instance and (optionally) an equipment + poll group. The view - below shows the first @PageSize tags by Name. -
- -
- - - Showing @VisibleRows.Count of @_rows.Count - -
- -
-
Tags
- @if (VisibleRows.Count == 0) - { -
No tags match the current filter.
- } - else - { -
- - - - - - - - - - - - - - - - @foreach (var t in VisibleRows) - { - - - - - - - - - - - - } - -
TagIdNameDriverEquipmentData typeAccessFolderPoll group
@t.TagId@t.Name@t.DriverInstanceId@(t.EquipmentId ?? "—")@t.DataType@t.AccessLevel@(t.FolderPath ?? "")@(t.PollGroupId ?? "—")Edit
-
- } -
-} - -@code { - private const int PageSize = 200; - - [Parameter] public string ClusterId { get; set; } = ""; - private List? _rows; - private string _filter = ""; - - private List VisibleRows => (_rows ?? new()) - .Where(t => string.IsNullOrWhiteSpace(_filter) - || t.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase)) - .Take(PageSize) - .ToList(); - - protected override async Task OnInitializedAsync() - { - await using var db = await DbFactory.CreateDbContextAsync(); - // Tags don't carry ClusterId; resolve via DriverInstance scoping. - var driverIds = db.DriverInstances.AsNoTracking() - .Where(d => d.ClusterId == ClusterId) - .Select(d => d.DriverInstanceId); - _rows = await db.Tags.AsNoTracking() - .Where(t => driverIds.Contains(t.DriverInstanceId)) - .OrderBy(t => t.Name) - .ToListAsync(); - } -} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterUns.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterUns.razor deleted file mode 100644 index ae950baa..00000000 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ClusterUns.razor +++ /dev/null @@ -1,106 +0,0 @@ -@page "/clusters/{ClusterId}/uns" -@attribute [Microsoft.AspNetCore.Authorization.Authorize] -@rendermode RenderMode.InteractiveServer -@using Microsoft.EntityFrameworkCore -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@inject IDbContextFactory DbFactory - -
-

UNS structure · @ClusterId

-
- - - -@if (_areas is null || _lines is null) -{ -

Loading…

-} -else -{ -
- UNS levels: Enterprise (cluster) → Site (cluster) → Area → Line → Equipment. Areas and - lines are cluster-scoped; equipment hangs under a single line. -
- -
-
- Areas (level 3) · @_areas.Count - New area -
- @if (_areas.Count == 0) - { -
No areas defined.
- } - else - { -
- - - - @foreach (var a in _areas) - { - - - - - - - } - -
UnsAreaIdNameNotes
@a.UnsAreaId@a.Name@(a.Notes ?? "")Edit
-
- } -
- -
-
- Lines (level 4) · @_lines.Count - New line -
- @if (_lines.Count == 0) - { -
No lines defined.
- } - else - { -
- - - - @foreach (var l in _lines) - { - - - - - - - - } - -
UnsLineIdNameAreaNotes
@l.UnsLineId@l.Name@l.UnsAreaId@(l.Notes ?? "")Edit
-
- } -
-} - -@code { - [Parameter] public string ClusterId { get; set; } = ""; - private List? _areas; - private List? _lines; - - protected override async Task OnInitializedAsync() - { - await using var db = await DbFactory.CreateDbContextAsync(); - _areas = await db.UnsAreas.AsNoTracking() - .Where(a => a.ClusterId == ClusterId) - .OrderBy(a => a.UnsAreaId) - .ToListAsync(); - var areaIds = _areas.Select(a => a.UnsAreaId).ToList(); - _lines = await db.UnsLines.AsNoTracking() - .Where(l => areaIds.Contains(l.UnsAreaId)) - .OrderBy(l => l.UnsAreaId).ThenBy(l => l.UnsLineId) - .ToListAsync(); - } -} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/EquipmentEdit.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/EquipmentEdit.razor deleted file mode 100644 index 8ab5b486..00000000 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/EquipmentEdit.razor +++ /dev/null @@ -1,309 +0,0 @@ -@page "/clusters/{ClusterId}/equipment/new" -@page "/clusters/{ClusterId}/equipment/{EquipmentId}" -@* Equipment CRUD. EquipmentId is system-generated (decision #125) — operator picks Name + - MachineCode + UnsLine + Driver; the EquipmentId is derived from the EquipmentUuid on create. - OPC 40010 identification fields (Manufacturer, Model, etc.) are all optional. *@ -@attribute [Microsoft.AspNetCore.Authorization.Authorize] -@rendermode RenderMode.InteractiveServer -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.EntityFrameworkCore -@using System.ComponentModel.DataAnnotations -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@inject IDbContextFactory DbFactory -@inject NavigationManager Nav - -
-

@(IsNew ? "New equipment" : "Edit equipment") · @ClusterId

- Cancel -
- - - -@if (!_loaded) -{ -

Loading…

-} -else if (!IsNew && _existing is null) -{ -
- Equipment @EquipmentId was not found. -
-} -else -{ - - -
-
Identity
-
- @if (!IsNew) - { -
- - -
System-generated; never operator-edited.
-
- } -
-
- - -
UNS level 5 segment; lowercase letters, digits, dashes, up to 32 chars.
-
-
- - -
-
-
-
- - - - @foreach (var l in _lines) - { - - } - -
-
- - - - @foreach (var d in _drivers) - { - - } - -
-
-
-
- - -
Unique fleet-wide via ExternalIdReservation.
-
-
- - -
-
-
- -
- - -
-
-
-
- -
-
OPC 40010 identification (optional)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - @if (!string.IsNullOrWhiteSpace(_error)) - { -
@_error
- } - -
- - Cancel - @if (!IsNew) - { - - } -
-
-} - -@code { - [Parameter] public string ClusterId { get; set; } = ""; - [Parameter] public string? EquipmentId { get; set; } - - private bool IsNew => string.IsNullOrEmpty(EquipmentId); - - private FormModel _form = new(); - private Equipment? _existing; - private List _lines = new(); - private List _drivers = new(); - private bool _loaded; - private bool _busy; - private string? _error; - - protected override async Task OnInitializedAsync() - { - await using var db = await DbFactory.CreateDbContextAsync(); - var areaIds = await db.UnsAreas.AsNoTracking() - .Where(a => a.ClusterId == ClusterId).Select(a => a.UnsAreaId).ToListAsync(); - _lines = await db.UnsLines.AsNoTracking() - .Where(l => areaIds.Contains(l.UnsAreaId)) - .OrderBy(l => l.UnsAreaId).ThenBy(l => l.UnsLineId) - .ToListAsync(); - _drivers = await db.DriverInstances.AsNoTracking() - .Where(d => d.ClusterId == ClusterId) - .OrderBy(d => d.DriverInstanceId) - .ToListAsync(); - - if (!IsNew) - { - _existing = await db.Equipment.AsNoTracking() - .FirstOrDefaultAsync(e => e.EquipmentId == EquipmentId); - if (_existing is not null) - { - _form = new FormModel - { - Name = _existing.Name, - MachineCode = _existing.MachineCode, - UnsLineId = _existing.UnsLineId, - DriverInstanceId = _existing.DriverInstanceId, - ZTag = _existing.ZTag, - SAPID = _existing.SAPID, - Manufacturer = _existing.Manufacturer, - Model = _existing.Model, - SerialNumber = _existing.SerialNumber, - HardwareRevision = _existing.HardwareRevision, - SoftwareRevision = _existing.SoftwareRevision, - YearOfConstruction = _existing.YearOfConstruction, - AssetLocation = _existing.AssetLocation, - ManufacturerUri = _existing.ManufacturerUri, - DeviceManualUri = _existing.DeviceManualUri, - Enabled = _existing.Enabled, - RowVersion = _existing.RowVersion, - }; - } - } - _loaded = true; - } - - private async Task SubmitAsync() - { - _busy = true; _error = null; - try - { - if (string.IsNullOrEmpty(_form.UnsLineId)) { _error = "Pick a UNS line."; return; } - - await using var db = await DbFactory.CreateDbContextAsync(); - if (IsNew) - { - var uuid = Guid.NewGuid(); - var equipmentId = $"EQ-{uuid.ToString("N")[..12]}"; - if (await db.Equipment.AnyAsync(e => e.MachineCode == _form.MachineCode)) - { _error = $"MachineCode '{_form.MachineCode}' already exists in this fleet."; return; } - db.Equipment.Add(new Equipment - { - EquipmentId = equipmentId, - EquipmentUuid = uuid, - DriverInstanceId = string.IsNullOrWhiteSpace(_form.DriverInstanceId) ? null : _form.DriverInstanceId, - UnsLineId = _form.UnsLineId, - Name = _form.Name, - MachineCode = _form.MachineCode, - ZTag = string.IsNullOrWhiteSpace(_form.ZTag) ? null : _form.ZTag, - SAPID = string.IsNullOrWhiteSpace(_form.SAPID) ? null : _form.SAPID, - Manufacturer = _form.Manufacturer, - Model = _form.Model, - SerialNumber = _form.SerialNumber, - HardwareRevision = _form.HardwareRevision, - SoftwareRevision = _form.SoftwareRevision, - YearOfConstruction = _form.YearOfConstruction, - AssetLocation = _form.AssetLocation, - ManufacturerUri = _form.ManufacturerUri, - DeviceManualUri = _form.DeviceManualUri, - Enabled = _form.Enabled, - }); - } - else - { - var entity = await db.Equipment.FirstOrDefaultAsync(e => e.EquipmentId == EquipmentId); - if (entity is null) { _error = "Row no longer exists."; return; } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - entity.DriverInstanceId = string.IsNullOrWhiteSpace(_form.DriverInstanceId) ? null : _form.DriverInstanceId; - entity.UnsLineId = _form.UnsLineId; - entity.Name = _form.Name; - entity.MachineCode = _form.MachineCode; - entity.ZTag = string.IsNullOrWhiteSpace(_form.ZTag) ? null : _form.ZTag; - entity.SAPID = string.IsNullOrWhiteSpace(_form.SAPID) ? null : _form.SAPID; - entity.Manufacturer = _form.Manufacturer; - entity.Model = _form.Model; - entity.SerialNumber = _form.SerialNumber; - entity.HardwareRevision = _form.HardwareRevision; - entity.SoftwareRevision = _form.SoftwareRevision; - entity.YearOfConstruction = _form.YearOfConstruction; - entity.AssetLocation = _form.AssetLocation; - entity.ManufacturerUri = _form.ManufacturerUri; - entity.DeviceManualUri = _form.DeviceManualUri; - entity.Enabled = _form.Enabled; - } - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/equipment"); - } - catch (DbUpdateConcurrencyException) { _error = "Another user changed this equipment while you were editing."; } - catch (Exception ex) { _error = ex.Message; } - finally { _busy = false; } - } - - private async Task DeleteAsync() - { - if (IsNew) return; - _busy = true; _error = null; - try - { - await using var db = await DbFactory.CreateDbContextAsync(); - var entity = await db.Equipment.FirstOrDefaultAsync(e => e.EquipmentId == EquipmentId); - if (entity is null) { Nav.NavigateTo($"/clusters/{ClusterId}/equipment"); return; } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - db.Equipment.Remove(entity); - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/equipment"); - } - catch (DbUpdateConcurrencyException) { _error = "Another user changed this equipment while you were viewing it."; } - catch (Exception ex) { _error = $"Delete failed: {ex.Message}. Likely because tags or virtual tags reference this equipment — remove them first."; } - finally { _busy = false; } - } - - private sealed class FormModel - { - [Required, RegularExpression("^[a-z0-9-]{1,32}$", ErrorMessage = "Lowercase letters, digits, dashes only; max 32 chars.")] - public string Name { get; set; } = ""; - [Required] public string MachineCode { get; set; } = ""; - [Required] public string UnsLineId { get; set; } = ""; - public string? DriverInstanceId { get; set; } - public string? ZTag { get; set; } - public string? SAPID { get; set; } - public string? Manufacturer { get; set; } - public string? Model { get; set; } - public string? SerialNumber { get; set; } - public string? HardwareRevision { get; set; } - public string? SoftwareRevision { get; set; } - public short? YearOfConstruction { get; set; } - public string? AssetLocation { get; set; } - public string? ManufacturerUri { get; set; } - public string? DeviceManualUri { get; set; } - public bool Enabled { get; set; } = true; - public byte[] RowVersion { get; set; } = []; - } -} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ImportEquipment.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ImportEquipment.razor deleted file mode 100644 index 60a80183..00000000 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/ImportEquipment.razor +++ /dev/null @@ -1,260 +0,0 @@ -@page "/clusters/{ClusterId}/equipment/import" -@* Bulk equipment import via pasted CSV. Header row required; columns: - Name, MachineCode, UnsLineId, DriverInstanceId, ZTag, SAPID, Manufacturer, Model - Empty optional columns parsed as null. EquipmentId is system-generated per row - (matches single-add path in EquipmentEdit.razor). *@ -@attribute [Microsoft.AspNetCore.Authorization.Authorize] -@rendermode RenderMode.InteractiveServer -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.EntityFrameworkCore -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@inject IDbContextFactory DbFactory -@inject NavigationManager Nav - -
-

Import equipment · @ClusterId

- Cancel -
- - - -
- Paste CSV below. Required header columns (in order): - Name, MachineCode, UnsLineId, DriverInstanceId. - Optional: ZTag, SAPID, Manufacturer, Model. - Bulk import requires a driver; driver-less (VirtualTag-only) equipment is created via the single-add form. - Each row inserts one Equipment with a freshly-generated EquipmentId. Existing rows are - detected by MachineCode and skipped (the importer is additive-only — no updates). -
- -
-
CSV
-
- -
-
- -@if (!string.IsNullOrWhiteSpace(_error)) -{ -
@_error
-} - -@if (_preview is not null) -{ -
-
Preview · @_preview.Count row@(_preview.Count == 1 ? "" : "s") to import
- @if (_preview.Count > 0) - { -
- - - - - - - - - - - - - - - - @foreach (var p in _preview) - { - - - - - - - - - - - - } - -
NameMachineCodeUNS lineDriverZTagSAPIDManufacturerModelStatus
@p.Name@p.MachineCode@p.UnsLineId@p.DriverInstanceId@(p.ZTag ?? "")@(p.SAPID ?? "")@(p.Manufacturer ?? "")@(p.Model ?? "") - @if (p.IsSkipped) { skip — exists } - else if (!string.IsNullOrEmpty(p.RowError)) { @p.RowError } - else { ready } -
-
- } -
-} - -
- - - Cancel -
- -@code { - [Parameter] public string ClusterId { get; set; } = ""; - - private string _csv = ""; - private List? _preview; - private bool _busy; - private string? _error; - - // Bulk import requires a DriverInstanceId by design — every CSV row must reference an existing driver. - // Driver-less equipment (DriverInstanceId == null) is not supported via bulk import; - // create it via the single-add editor (/clusters/{id}/equipment/new) or the SQL loader. - private static readonly string[] RequiredColumns = ["Name", "MachineCode", "UnsLineId", "DriverInstanceId"]; - private static readonly string[] OptionalColumns = ["ZTag", "SAPID", "Manufacturer", "Model"]; - - private async Task PreviewAsync() - { - _busy = true; - _error = null; - _preview = null; - try - { - var parsed = ParseCsv(_csv); - if (parsed is null) return; - - await using var db = await DbFactory.CreateDbContextAsync(); - var driversInCluster = await db.DriverInstances.AsNoTracking() - .Where(d => d.ClusterId == ClusterId) - .Select(d => d.DriverInstanceId) - .ToListAsync(); - var driverSet = driversInCluster.ToHashSet(StringComparer.Ordinal); - var areaIds = await db.UnsAreas.AsNoTracking() - .Where(a => a.ClusterId == ClusterId) - .Select(a => a.UnsAreaId).ToListAsync(); - var validLines = await db.UnsLines.AsNoTracking() - .Where(l => areaIds.Contains(l.UnsAreaId)) - .Select(l => l.UnsLineId).ToListAsync(); - var lineSet = validLines.ToHashSet(StringComparer.Ordinal); - var existingMachineCodes = await db.Equipment.AsNoTracking() - .Select(e => e.MachineCode).ToListAsync(); - var existingSet = existingMachineCodes.ToHashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var row in parsed) - { - if (existingSet.Contains(row.MachineCode)) - { - row.IsSkipped = true; - continue; - } - if (!driverSet.Contains(row.DriverInstanceId)) - { - row.RowError = $"driver '{row.DriverInstanceId}' not in this cluster"; - continue; - } - if (!lineSet.Contains(row.UnsLineId)) - { - row.RowError = $"UNS line '{row.UnsLineId}' not in this cluster"; - } - } - _preview = parsed; - } - catch (Exception ex) { _error = ex.Message; } - finally { _busy = false; } - } - - private async Task ImportAsync() - { - if (_preview is null) return; - _busy = true; - _error = null; - try - { - await using var db = await DbFactory.CreateDbContextAsync(); - var added = 0; - foreach (var row in _preview.Where(p => !p.IsSkipped && string.IsNullOrEmpty(p.RowError))) - { - var uuid = Guid.NewGuid(); - var equipmentId = $"EQ-{uuid.ToString("N")[..12]}"; - db.Equipment.Add(new Equipment - { - EquipmentId = equipmentId, - EquipmentUuid = uuid, - DriverInstanceId = row.DriverInstanceId, - UnsLineId = row.UnsLineId, - Name = row.Name, - MachineCode = row.MachineCode, - ZTag = row.ZTag, - SAPID = row.SAPID, - Manufacturer = row.Manufacturer, - Model = row.Model, - Enabled = true, - }); - added++; - } - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/equipment"); - } - catch (Exception ex) { _error = ex.Message; } - finally { _busy = false; } - } - - private List? ParseCsv(string csv) - { - if (string.IsNullOrWhiteSpace(csv)) { _error = "CSV is empty."; return null; } - var lines = csv.Replace("\r\n", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries); - if (lines.Length < 2) { _error = "Need a header row and at least one data row."; return null; } - - var header = lines[0].Split(',').Select(c => c.Trim()).ToArray(); - for (var i = 0; i < RequiredColumns.Length; i++) - { - if (i >= header.Length || !string.Equals(header[i], RequiredColumns[i], StringComparison.OrdinalIgnoreCase)) - { - _error = $"Header column #{i + 1} must be '{RequiredColumns[i]}' (got '{(i < header.Length ? header[i] : "")}')."; - return null; - } - } - - var rows = new List(); - for (var lineIdx = 1; lineIdx < lines.Length; lineIdx++) - { - var parts = lines[lineIdx].Split(',').Select(c => c.Trim()).ToArray(); - if (parts.Length < RequiredColumns.Length) - { - rows.Add(new PreviewRow { RowError = $"too few columns (got {parts.Length}, need {RequiredColumns.Length})" }); - continue; - } - rows.Add(new PreviewRow - { - Name = parts[0], - MachineCode = parts[1], - UnsLineId = parts[2], - DriverInstanceId = parts[3], - ZTag = NullIfEmpty(parts, 4), - SAPID = NullIfEmpty(parts, 5), - Manufacturer = NullIfEmpty(parts, 6), - Model = NullIfEmpty(parts, 7), - }); - } - return rows; - } - - private static string? NullIfEmpty(string[] parts, int idx) => - idx < parts.Length && !string.IsNullOrWhiteSpace(parts[idx]) ? parts[idx] : null; - - private sealed class PreviewRow - { - public string Name { get; set; } = ""; - public string MachineCode { get; set; } = ""; - public string UnsLineId { get; set; } = ""; - public string DriverInstanceId { get; set; } = ""; - public string? ZTag { get; set; } - public string? SAPID { get; set; } - public string? Manufacturer { get; set; } - public string? Model { get; set; } - public bool IsSkipped { get; set; } - public string? RowError { get; set; } - } -} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/TagEdit.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/TagEdit.razor deleted file mode 100644 index ba142ce2..00000000 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/TagEdit.razor +++ /dev/null @@ -1,320 +0,0 @@ -@page "/clusters/{ClusterId}/tags/new" -@page "/clusters/{ClusterId}/tags/{TagId}" -@* Tag CRUD. EquipmentId is required when the chosen driver's namespace is Equipment-kind, - forbidden when SystemPlatform-kind (decision #110); the form switches between - "pick equipment" and "FolderPath input" based on namespace kind. *@ -@attribute [Microsoft.AspNetCore.Authorization.Authorize] -@rendermode RenderMode.InteractiveServer -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.EntityFrameworkCore -@using System.ComponentModel.DataAnnotations -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@using ZB.MOM.WW.OtOpcUa.Configuration.Enums -@inject IDbContextFactory DbFactory -@inject NavigationManager Nav - -
-

@(IsNew ? "New tag" : "Edit tag") · @ClusterId

- Cancel -
- - - -@if (!_loaded) -{ -

Loading…

-} -else if (!IsNew && _existing is null) -{ -
- Tag @TagId was not found. -
-} -else -{ - - -
-
Identity
-
-
-
- - -
-
- - -
-
-
-
- - - - @foreach (var d in _drivers) - { - - } - -
-
- - - @foreach (var dt in DataTypes) - { - - } - -
-
- - @{ var driverNamespace = ResolveDriverNamespace(_form.DriverInstanceId); } - @if (driverNamespace?.Kind == NamespaceKind.Equipment) - { -
- - - - @foreach (var e in _equipment.Where(e => e.DriverInstanceId == _form.DriverInstanceId)) - { - - } - -
- } - else if (driverNamespace?.Kind == NamespaceKind.SystemPlatform) - { -
- - -
Galaxy hierarchy preserved as v1 expressed it — no UNS rule.
-
- } - else if (!string.IsNullOrEmpty(_form.DriverInstanceId)) - { -
Pick a driver to see its namespace kind.
- } - -
-
- - - - - -
-
- -
- - -
-
-
-
- - -
-
-
- -
-
Tag config (JSON)
-
- -
Schemaless per driver type — register / address / scaling / byte-order. Validated server-side at deploy.
-
-
- - @if (!string.IsNullOrWhiteSpace(_error)) - { -
@_error
- } - -
- - Cancel - @if (!IsNew) - { - - } -
-
-} - -@code { - private static readonly string[] DataTypes = - ["Boolean", "SByte", "Byte", "Int16", "UInt16", "Int32", "UInt32", - "Int64", "UInt64", "Float", "Double", "String", "DateTime", "Guid", "ByteString"]; - - [Parameter] public string ClusterId { get; set; } = ""; - [Parameter] public string? TagId { get; set; } - - private bool IsNew => string.IsNullOrEmpty(TagId); - - private FormModel _form = new(); - private Tag? _existing; - private List _drivers = new(); - private List _equipment = new(); - private Dictionary _namespacesByDriverInstance = new(); - private bool _loaded; - private bool _busy; - private string? _error; - - protected override async Task OnInitializedAsync() - { - await using var db = await DbFactory.CreateDbContextAsync(); - _drivers = await db.DriverInstances.AsNoTracking() - .Where(d => d.ClusterId == ClusterId) - .OrderBy(d => d.DriverInstanceId) - .ToListAsync(); - var namespaces = await db.Namespaces.AsNoTracking() - .Where(n => n.ClusterId == ClusterId) - .ToListAsync(); - var nsById = namespaces.ToDictionary(n => n.NamespaceId); - _namespacesByDriverInstance = _drivers.ToDictionary( - d => d.DriverInstanceId, - d => nsById.TryGetValue(d.NamespaceId, out var ns) ? ns : namespaces.First()); - var driverIds = _drivers.Select(d => d.DriverInstanceId).ToHashSet(); - _equipment = await db.Equipment.AsNoTracking() - .Where(e => e.DriverInstanceId != null && driverIds.Contains(e.DriverInstanceId)) - .OrderBy(e => e.MachineCode) - .ToListAsync(); - - if (!IsNew) - { - _existing = await db.Tags.AsNoTracking() - .FirstOrDefaultAsync(t => t.TagId == TagId); - if (_existing is not null) - { - _form = new FormModel - { - TagId = _existing.TagId, - Name = _existing.Name, - DriverInstanceId = _existing.DriverInstanceId, - EquipmentId = _existing.EquipmentId, - FolderPath = _existing.FolderPath, - DataType = _existing.DataType, - AccessLevel = _existing.AccessLevel, - WriteIdempotent = _existing.WriteIdempotent, - PollGroupId = _existing.PollGroupId, - TagConfig = _existing.TagConfig, - RowVersion = _existing.RowVersion, - }; - } - } - else - { - _form.DataType = "Float"; - _form.AccessLevel = TagAccessLevel.Read; - _form.TagConfig = "{}"; - } - _loaded = true; - } - - private Namespace? ResolveDriverNamespace(string driverId) => - string.IsNullOrEmpty(driverId) ? null - : _namespacesByDriverInstance.TryGetValue(driverId, out var ns) ? ns : null; - - private async Task SubmitAsync() - { - _busy = true; _error = null; - try - { - if (string.IsNullOrEmpty(_form.DriverInstanceId)) { _error = "Pick a driver."; return; } - var ns = ResolveDriverNamespace(_form.DriverInstanceId); - if (ns?.Kind == NamespaceKind.Equipment && string.IsNullOrEmpty(_form.EquipmentId)) - { _error = "Driver lives in an Equipment-kind namespace — pick an equipment."; return; } - if (ns?.Kind == NamespaceKind.SystemPlatform && !string.IsNullOrEmpty(_form.EquipmentId)) - { _error = "Driver lives in a SystemPlatform namespace — EquipmentId must be empty (use FolderPath)."; return; } - - try { using var _ = System.Text.Json.JsonDocument.Parse(_form.TagConfig); } - catch { _error = "TagConfig is not valid JSON."; return; } - - await using var db = await DbFactory.CreateDbContextAsync(); - if (IsNew) - { - if (await db.Tags.AnyAsync(t => t.TagId == _form.TagId)) - { _error = $"Tag '{_form.TagId}' already exists."; return; } - db.Tags.Add(new Tag - { - TagId = _form.TagId, - DriverInstanceId = _form.DriverInstanceId, - EquipmentId = string.IsNullOrEmpty(_form.EquipmentId) ? null : _form.EquipmentId, - Name = _form.Name, - FolderPath = string.IsNullOrWhiteSpace(_form.FolderPath) ? null : _form.FolderPath, - DataType = _form.DataType, - AccessLevel = _form.AccessLevel, - WriteIdempotent = _form.WriteIdempotent, - PollGroupId = string.IsNullOrWhiteSpace(_form.PollGroupId) ? null : _form.PollGroupId, - TagConfig = _form.TagConfig, - }); - } - else - { - var entity = await db.Tags.FirstOrDefaultAsync(t => t.TagId == TagId); - if (entity is null) { _error = "Row no longer exists."; return; } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - entity.DriverInstanceId = _form.DriverInstanceId; - entity.EquipmentId = string.IsNullOrEmpty(_form.EquipmentId) ? null : _form.EquipmentId; - entity.Name = _form.Name; - entity.FolderPath = string.IsNullOrWhiteSpace(_form.FolderPath) ? null : _form.FolderPath; - entity.DataType = _form.DataType; - entity.AccessLevel = _form.AccessLevel; - entity.WriteIdempotent = _form.WriteIdempotent; - entity.PollGroupId = string.IsNullOrWhiteSpace(_form.PollGroupId) ? null : _form.PollGroupId; - entity.TagConfig = _form.TagConfig; - } - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/tags"); - } - catch (DbUpdateConcurrencyException) { _error = "Another user changed this tag while you were editing."; } - catch (Exception ex) { _error = ex.Message; } - finally { _busy = false; } - } - - private async Task DeleteAsync() - { - if (IsNew) return; - _busy = true; _error = null; - try - { - await using var db = await DbFactory.CreateDbContextAsync(); - var entity = await db.Tags.FirstOrDefaultAsync(t => t.TagId == TagId); - if (entity is null) { Nav.NavigateTo($"/clusters/{ClusterId}/tags"); return; } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - db.Tags.Remove(entity); - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/tags"); - } - catch (DbUpdateConcurrencyException) { _error = "Another user changed this tag while you were viewing it."; } - catch (Exception ex) { _error = ex.Message; } - finally { _busy = false; } - } - - 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? EquipmentId { get; set; } - public string? FolderPath { get; set; } - [Required] 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; } = "{}"; - public byte[] RowVersion { get; set; } = []; - } -} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/UnsAreaEdit.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/UnsAreaEdit.razor deleted file mode 100644 index 632a6988..00000000 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/UnsAreaEdit.razor +++ /dev/null @@ -1,167 +0,0 @@ -@page "/clusters/{ClusterId}/uns/areas/new" -@page "/clusters/{ClusterId}/uns/areas/{UnsAreaId}" -@attribute [Microsoft.AspNetCore.Authorization.Authorize] -@rendermode RenderMode.InteractiveServer -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.EntityFrameworkCore -@using System.ComponentModel.DataAnnotations -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@inject IDbContextFactory DbFactory -@inject NavigationManager Nav - -
-

@(IsNew ? "New UNS area" : "Edit UNS area") · @ClusterId

- Cancel -
- - - -@if (!_loaded) -{ -

Loading…

-} -else if (!IsNew && _existing is null) -{ -
- Area @UnsAreaId was not found. -
-} -else -{ - - -
-
UNS area (level 3)
-
-
- - -
-
- - -
-
- - -
-
-
- - @if (!string.IsNullOrWhiteSpace(_error)) - { -
@_error
- } - -
- - Cancel - @if (!IsNew) - { - - } -
-
-} - -@code { - [Parameter] public string ClusterId { get; set; } = ""; - [Parameter] public string? UnsAreaId { get; set; } - - private bool IsNew => string.IsNullOrEmpty(UnsAreaId); - - private FormModel _form = new(); - private UnsArea? _existing; - private bool _loaded; - private bool _busy; - private string? _error; - - protected override async Task OnInitializedAsync() - { - if (!IsNew) - { - await using var db = await DbFactory.CreateDbContextAsync(); - _existing = await db.UnsAreas.AsNoTracking() - .FirstOrDefaultAsync(a => a.ClusterId == ClusterId && a.UnsAreaId == UnsAreaId); - if (_existing is not null) - { - _form = new FormModel - { - UnsAreaId = _existing.UnsAreaId, - Name = _existing.Name, - Notes = _existing.Notes, - RowVersion = _existing.RowVersion, - }; - } - } - _loaded = true; - } - - private async Task SubmitAsync() - { - _busy = true; _error = null; - try - { - await using var db = await DbFactory.CreateDbContextAsync(); - if (IsNew) - { - if (await db.UnsAreas.AnyAsync(a => a.UnsAreaId == _form.UnsAreaId)) - { _error = $"Area '{_form.UnsAreaId}' already exists."; return; } - db.UnsAreas.Add(new UnsArea - { - UnsAreaId = _form.UnsAreaId, - ClusterId = ClusterId, - Name = _form.Name, - Notes = string.IsNullOrWhiteSpace(_form.Notes) ? null : _form.Notes, - }); - } - else - { - var entity = await db.UnsAreas.FirstOrDefaultAsync( - a => a.ClusterId == ClusterId && a.UnsAreaId == UnsAreaId); - if (entity is null) { _error = "Row no longer exists."; return; } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - entity.Name = _form.Name; - entity.Notes = string.IsNullOrWhiteSpace(_form.Notes) ? null : _form.Notes; - } - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/uns"); - } - catch (DbUpdateConcurrencyException) { _error = "Another user changed this area while you were editing. Reload to see the latest values."; } - catch (Exception ex) { _error = ex.Message; } - finally { _busy = false; } - } - - private async Task DeleteAsync() - { - if (IsNew) return; - _busy = true; _error = null; - try - { - await using var db = await DbFactory.CreateDbContextAsync(); - var entity = await db.UnsAreas.FirstOrDefaultAsync( - a => a.ClusterId == ClusterId && a.UnsAreaId == UnsAreaId); - if (entity is null) { Nav.NavigateTo($"/clusters/{ClusterId}/uns"); return; } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - db.UnsAreas.Remove(entity); - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/uns"); - } - catch (DbUpdateConcurrencyException) { _error = "Another user changed this area while you were viewing it."; } - catch (Exception ex) { _error = $"Delete failed: {ex.Message}. Likely because lines still reference this area — remove them first."; } - finally { _busy = false; } - } - - private sealed class FormModel - { - [Required, RegularExpression("^[A-Za-z0-9_-]+$")] public string UnsAreaId { get; set; } = ""; - [Required] public string Name { get; set; } = ""; - public string? Notes { get; set; } - public byte[] RowVersion { get; set; } = []; - } -} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/UnsLineEdit.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/UnsLineEdit.razor deleted file mode 100644 index 729ae6dd..00000000 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/UnsLineEdit.razor +++ /dev/null @@ -1,187 +0,0 @@ -@page "/clusters/{ClusterId}/uns/lines/new" -@page "/clusters/{ClusterId}/uns/lines/{UnsLineId}" -@attribute [Microsoft.AspNetCore.Authorization.Authorize] -@rendermode RenderMode.InteractiveServer -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.EntityFrameworkCore -@using System.ComponentModel.DataAnnotations -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@inject IDbContextFactory DbFactory -@inject NavigationManager Nav - -
-

@(IsNew ? "New UNS line" : "Edit UNS line") · @ClusterId

- Cancel -
- - - -@if (!_loaded) -{ -

Loading…

-} -else if (!IsNew && _existing is null) -{ -
- Line @UnsLineId was not found. -
-} -else -{ - - -
-
UNS line (level 4)
-
-
- - -
-
- - - @foreach (var area in _areas) - { - - } - -
-
- - -
-
- - -
-
-
- - @if (!string.IsNullOrWhiteSpace(_error)) - { -
@_error
- } - -
- - Cancel - @if (!IsNew) - { - - } -
-
-} - -@code { - [Parameter] public string ClusterId { get; set; } = ""; - [Parameter] public string? UnsLineId { get; set; } - - private bool IsNew => string.IsNullOrEmpty(UnsLineId); - - private FormModel _form = new(); - private UnsLine? _existing; - private List _areas = new(); - private bool _loaded; - private bool _busy; - private string? _error; - - protected override async Task OnInitializedAsync() - { - await using var db = await DbFactory.CreateDbContextAsync(); - _areas = await db.UnsAreas.AsNoTracking() - .Where(a => a.ClusterId == ClusterId) - .OrderBy(a => a.UnsAreaId) - .ToListAsync(); - - if (!IsNew) - { - _existing = await db.UnsLines.AsNoTracking() - .FirstOrDefaultAsync(l => l.UnsLineId == UnsLineId); - if (_existing is not null) - { - _form = new FormModel - { - UnsLineId = _existing.UnsLineId, - UnsAreaId = _existing.UnsAreaId, - Name = _existing.Name, - Notes = _existing.Notes, - RowVersion = _existing.RowVersion, - }; - } - } - else - { - _form.UnsAreaId = _areas.FirstOrDefault()?.UnsAreaId ?? ""; - } - _loaded = true; - } - - private async Task SubmitAsync() - { - _busy = true; _error = null; - try - { - await using var db = await DbFactory.CreateDbContextAsync(); - if (IsNew) - { - if (await db.UnsLines.AnyAsync(l => l.UnsLineId == _form.UnsLineId)) - { _error = $"Line '{_form.UnsLineId}' already exists."; return; } - db.UnsLines.Add(new UnsLine - { - UnsLineId = _form.UnsLineId, - UnsAreaId = _form.UnsAreaId, - Name = _form.Name, - Notes = string.IsNullOrWhiteSpace(_form.Notes) ? null : _form.Notes, - }); - } - else - { - var entity = await db.UnsLines.FirstOrDefaultAsync(l => l.UnsLineId == UnsLineId); - if (entity is null) { _error = "Row no longer exists."; return; } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - entity.UnsAreaId = _form.UnsAreaId; - entity.Name = _form.Name; - entity.Notes = string.IsNullOrWhiteSpace(_form.Notes) ? null : _form.Notes; - } - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/uns"); - } - catch (DbUpdateConcurrencyException) { _error = "Another user changed this line while you were editing."; } - catch (Exception ex) { _error = ex.Message; } - finally { _busy = false; } - } - - private async Task DeleteAsync() - { - if (IsNew) return; - _busy = true; _error = null; - try - { - await using var db = await DbFactory.CreateDbContextAsync(); - var entity = await db.UnsLines.FirstOrDefaultAsync(l => l.UnsLineId == UnsLineId); - if (entity is null) { Nav.NavigateTo($"/clusters/{ClusterId}/uns"); return; } - db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; - db.UnsLines.Remove(entity); - await db.SaveChangesAsync(); - Nav.NavigateTo($"/clusters/{ClusterId}/uns"); - } - catch (DbUpdateConcurrencyException) { _error = "Another user changed this line while you were viewing it."; } - catch (Exception ex) { _error = $"Delete failed: {ex.Message}. Likely because equipment still references this line — remove or re-home it first."; } - finally { _busy = false; } - } - - private sealed class FormModel - { - [Required, RegularExpression("^[A-Za-z0-9_-]+$")] public string UnsLineId { get; set; } = ""; - [Required] public string UnsAreaId { get; set; } = ""; - [Required] public string Name { get; set; } = ""; - public string? Notes { get; set; } - public byte[] RowVersion { get; set; } = []; - } -} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/VirtualTagEdit.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/VirtualTagEdit.razor deleted file mode 100644 index 7bf62279..00000000 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/VirtualTagEdit.razor +++ /dev/null @@ -1,231 +0,0 @@ -@page "/virtual-tags/new" -@page "/virtual-tags/{VirtualTagId}" -@attribute [Microsoft.AspNetCore.Authorization.Authorize] -@rendermode RenderMode.InteractiveServer -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.EntityFrameworkCore -@using System.ComponentModel.DataAnnotations -@using ZB.MOM.WW.OtOpcUa.Configuration -@using ZB.MOM.WW.OtOpcUa.Configuration.Entities -@inject IDbContextFactory DbFactory -@inject NavigationManager Nav - -
-

@(IsNew ? "New virtual tag" : "Edit virtual tag")

- Cancel -
- -@if (!_loaded) -{ -

Loading…

-} -else if (!IsNew && _existing is null) -{ -
@VirtualTagId not found.
-} -else -{ - - -
-
Identity
-
-
-
- - -
-
- - -
-
-
-
- - - - @foreach (var e in _equipment) { } - -
-
- - -
-
- - - - @foreach (var s in _scripts) { } - -
-
-
-
- -
- - -
-
-
- - -
Periodic re-evaluation. Null = change-trigger only.
-
-
- -
- - -
-
-
-
- -
- - -
-
-
-
- - @if (!string.IsNullOrWhiteSpace(_error)) {
@_error
} - -
- - Cancel - @if (!IsNew) { } -
-
-} - -@code { - [Parameter] public string? VirtualTagId { get; set; } - private bool IsNew => string.IsNullOrEmpty(VirtualTagId); - - private FormModel _form = new(); - private VirtualTag? _existing; - private List _equipment = new(); - private List