From ac69a1c39d1811e2634f956bf3a41d2df842ebf3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 19 Apr 2026 22:00:40 -0400 Subject: [PATCH] =?UTF-8?q?Equipment=20CSV=20import=20UI=20=E2=80=94=20Str?= =?UTF-8?q?eam=20B.3/B.5=20operator=20page=20+=20EquipmentTab=20"Import=20?= =?UTF-8?q?CSV"=20button.=20Closes=20the=20UI=20slice=20of=20task=20#163?= =?UTF-8?q?=20(Phase=206.4=20Stream=20B.3/B.5);=20the=20ExternalIdReservat?= =?UTF-8?q?ion=20merge=20follow-up=20inside=20FinaliseBatchAsync=20is=20sp?= =?UTF-8?q?lit=20into=20new=20task=20#197=20so=20it=20gets=20a=20proper=20?= =?UTF-8?q?concurrent-insert=20test=20matrix=20rather=20than=20riding=20th?= =?UTF-8?q?is=20UI=20PR.=20New=20/clusters/{ClusterId}/draft/{GenerationId?= =?UTF-8?q?}/import-equipment=20page=20driving=20the=20full=20staged-impor?= =?UTF-8?q?t=20flow=20end-to-end.=20Operator=20selects=20a=20driver=20inst?= =?UTF-8?q?ance=20+=20UNS=20line=20(both=20scoped=20to=20the=20draft=20gen?= =?UTF-8?q?eration=20via=20DriverInstanceService.ListAsync=20+=20UnsServic?= =?UTF-8?q?e.ListLinesAsync=20dropdowns),=20pastes=20or=20uploads=20a=20CS?= =?UTF-8?q?V=20(InputFile=20with=205=20MiB=20cap=20so=20pathological=20fil?= =?UTF-8?q?es=20can't=20OOM=20the=20server),=20clicks=20Parse=20=E2=80=94?= =?UTF-8?q?=20EquipmentCsvImporter.Parse=20runs=20+=20shows=20two=20side-b?= =?UTF-8?q?y-side=20cards=20(accepted=20rows=20in=20green=20with=20ZTag/Ma?= =?UTF-8?q?chine/Name/Line=20columns,=20rejected=20rows=20in=20red=20with?= =?UTF-8?q?=20line-number=20+=20reason).=20Click=20Stage=20+=20Finalise=20?= =?UTF-8?q?and=20the=20page=20calls=20CreateBatchAsync=20=E2=86=92=20Stage?= =?UTF-8?q?RowsAsync=20=E2=86=92=20FinaliseBatchAsync=20in=20sequence=20us?= =?UTF-8?q?ing=20the=20authenticated=20user's=20identity=20as=20CreatedBy;?= =?UTF-8?q?=20on=20success,=20600ms=20banner=20then=20NavigateTo=20back=20?= =?UTF-8?q?to=20the=20draft=20editor=20so=20operator=20sees=20the=20newly-?= =?UTF-8?q?imported=20rows=20in=20EquipmentTab=20without=20a=20manual=20re?= =?UTF-8?q?fresh.=20Parse=20errors=20(missing=20version=20marker,=20bad=20?= =?UTF-8?q?header,=20malformed=20CSV)=20surface=20InvalidCsvFormatExceptio?= =?UTF-8?q?n.Message=20inline=20alongside=20the=20Parse=20button=20?= =?UTF-8?q?=E2=80=94=20no=20page=20reload=20needed=20to=20retry.=20Finalis?= =?UTF-8?q?e=20errors=20surface=20the=20service-layer=20exception=20messag?= =?UTF-8?q?e=20(ImportBatchNotFoundException=20/=20ImportBatchAlreadyFinal?= =?UTF-8?q?isedException=20/=20any=20DbUpdate*=20exception=20from=20the=20?= =?UTF-8?q?atomic=20transaction)=20so=20operator=20sees=20exactly=20why=20?= =?UTF-8?q?the=20finalise=20rejected=20before=20the=20tx=20rolled=20back.?= =?UTF-8?q?=20EquipmentTab=20gains=20an=20"Import=20CSV=E2=80=A6"=20button?= =?UTF-8?q?=20next=20to=20"Add=20equipment"=20that=20NavigateTo's=20the=20?= =?UTF-8?q?new=20page;=20it=20needs=20a=20ClusterId=20parameter=20to=20bui?= =?UTF-8?q?ld=20the=20URL=20so=20the=20@code=20block=20adds=20[Parameter]?= =?UTF-8?q?=20string=20ClusterId,=20and=20DraftEditor=20now=20passes=20Clu?= =?UTF-8?q?sterId=3D"@ClusterId"=20alongside=20the=20existing=20Generation?= =?UTF-8?q?Id.=20EquipmentImportBatchService=20was=20already=20implemented?= =?UTF-8?q?=20in=20Phase=206.4=20Stream=20B.4=20but=20missing=20from=20the?= =?UTF-8?q?=20Admin=20DI=20container=20=E2=80=94=20this=20PR=20adds=20AddS?= =?UTF-8?q?coped=20so=20the=20@inject=20resolves.=20The=20FinaliseBatch=20?= =?UTF-8?q?docstring=20explicitly=20defers=20ExternalIdReservation=20merge?= =?UTF-8?q?=20as=20a=20narrower=20follow-up=20with=20a=20concurrent-insert?= =?UTF-8?q?=20test=20matrix=20=E2=80=94=20task=20#197=20captures=20that=20?= =?UTF-8?q?work.=20For=20now=20the=20finalise=20may=20surface=20a=20DB-lev?= =?UTF-8?q?el=20UNIQUE-constraint=20violation=20if=20a=20ZTag=20conflict?= =?UTF-8?q?=20exists=20at=20commit=20time;=20the=20UI=20shows=20the=20raw?= =?UTF-8?q?=20message=20+=20the=20batch=20+=20staged=20rows=20are=20still?= =?UTF-8?q?=20in=20the=20DB=20for=20re-use=20once=20the=20conflict=20is=20?= =?UTF-8?q?resolved.=20Admin=20project=20builds=200=20errors;=20Admin.Test?= =?UTF-8?q?s=2072/72=20passing.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Pages/Clusters/DraftEditor.razor | 2 +- .../Pages/Clusters/EquipmentTab.razor | 9 +- .../Pages/Clusters/ImportEquipment.razor | 200 ++++++++++++++++++ src/ZB.MOM.WW.OtOpcUa.Admin/Program.cs | 1 + 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ImportEquipment.razor diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor index 49d90eb..dfef4b9 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor +++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DraftEditor.razor @@ -27,7 +27,7 @@
- @if (_tab == "equipment") { } + @if (_tab == "equipment") { } else if (_tab == "uns") { } else if (_tab == "namespaces") { } else if (_tab == "drivers") { } diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/EquipmentTab.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/EquipmentTab.razor index 4bc3d4f..109f5b6 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/EquipmentTab.razor +++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/EquipmentTab.razor @@ -2,10 +2,14 @@ @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Configuration.Validation @inject EquipmentService EquipmentSvc +@inject NavigationManager Nav

Equipment (draft gen @GenerationId)

- +
+ + +
@if (_equipment is null) @@ -96,6 +100,9 @@ else if (_equipment.Count > 0) @code { [Parameter] public long GenerationId { get; set; } + [Parameter] public string ClusterId { get; set; } = string.Empty; + + private void GoImport() => Nav.NavigateTo($"/clusters/{ClusterId}/draft/{GenerationId}/import-equipment"); private List? _equipment; private bool _showForm; private bool _editMode; diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ImportEquipment.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ImportEquipment.razor new file mode 100644 index 0000000..f7b7b54 --- /dev/null +++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ImportEquipment.razor @@ -0,0 +1,200 @@ +@page "/clusters/{ClusterId}/draft/{GenerationId:long}/import-equipment" +@using Microsoft.AspNetCore.Components.Authorization +@using ZB.MOM.WW.OtOpcUa.Admin.Services +@using ZB.MOM.WW.OtOpcUa.Configuration.Entities +@inject DriverInstanceService DriverSvc +@inject UnsService UnsSvc +@inject EquipmentImportBatchService BatchSvc +@inject NavigationManager Nav +@inject AuthenticationStateProvider AuthProvider + +
+
+

Equipment CSV import

+ Cluster @ClusterId · draft generation @GenerationId +
+ Back to draft +
+ +
+ Accepts @EquipmentCsvImporter.VersionMarker-headered CSV per Stream B.3. + Required columns: @string.Join(", ", EquipmentCsvImporter.RequiredColumns). + Optional columns cover the OPC 40010 Identification fields. Paste the file contents + or upload directly — the parser runs client-stream-side and shows a row-level preview + before anything lands in the draft. ZTag + SAPID uniqueness across the fleet is NOT + enforced here yet (see task #197); for now the finalise may fail at commit time if a + reservation conflict exists. +
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +