59ea02d971
Staged CSV tag import (upload -> parse -> review grid with per-row verdicts -> all-or-nothing commit) and export for the /raw tree Device/TagGroup. - CsvColumnMap: per-driver typed column <-> TagConfig JSON maps (model-backed for the 7 typed drivers, raw-key for Galaxy/Calculation) + the common column dictionary; typed columns merge OVER the TagConfigJson fallback (typed wins). - RawTagCsvMapper: pure parse (review rows + verdicts + typed/fallback provenance + intra-batch dup detection) and export (residual TagConfigJson) over the T0-2 CsvParser/CsvWriter. - RawTagCsvExportReader: DbContext-backed read helper (no IRawTreeService extension) enumerating a device's tags + reconstructing group paths. - RawCsvImportModal + RawCsvExportModal (data-URI download). - Reflection guard test (typed column -> real model property + batch-plan dictionary parity) + export->import round-trip property test. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
171 lines
6.3 KiB
Plaintext
171 lines
6.3 KiB
Plaintext
@* CSV tag export for a Raw-tree Device / TagGroup (B2-WP5).
|
|
|
|
Reads the device's tags (via the WP5-owned RawTagCsvExportReader — no IRawTreeService extension) and
|
|
renders them in the same column shape RawCsvImportModal consumes (T0-2 CsvWriter, per-driver
|
|
CsvColumnMap). The CSV is offered as a self-contained data-URI download anchor (no JS interop, no
|
|
external host — matches the app's CSP posture) plus a preview textarea.
|
|
|
|
Modal contract (the RawTree coordinator wires this in place of the OnExportCsv stub):
|
|
<RawCsvExportModal @bind-Visible="_exportVisible"
|
|
DeviceId="@_exportDeviceId" (may be blank if TagGroupId is supplied)
|
|
TagGroupId="@_exportTagGroupId" (when set, only that group's subtree is exported)
|
|
DriverType="@_exportDriverType" /> *@
|
|
@using System.Text
|
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
|
@inject RawTagCsvExportReader ExportReader
|
|
|
|
@if (Visible)
|
|
{
|
|
<div class="modal-backdrop fade show" style="display:block"></div>
|
|
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Export tags — CSV</h5>
|
|
<button type="button" class="btn-close" aria-label="Close" @onclick="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
@if (_loading)
|
|
{
|
|
<p class="text-muted"><span class="spinner-border spinner-border-sm"></span> Reading tags…</p>
|
|
}
|
|
else if (_error is not null)
|
|
{
|
|
<div class="alert alert-danger">@_error</div>
|
|
}
|
|
else
|
|
{
|
|
<p class="text-muted">
|
|
<span class="badge bg-secondary">@_rowCount tags</span>
|
|
for <span class="mono">@DriverType</span> device
|
|
@if (!string.IsNullOrEmpty(_groupFilter))
|
|
{
|
|
<span>, group <span class="mono">@_groupFilter</span></span>
|
|
}
|
|
</p>
|
|
<textarea class="form-control mono small" rows="10" readonly>@_csv</textarea>
|
|
}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" @onclick="Close">Close</button>
|
|
@if (!_loading && _error is null)
|
|
{
|
|
<a class="btn btn-primary" download="@_fileName" href="@_dataUri">Download CSV</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
/// <summary>Whether the modal is shown (two-way bound).</summary>
|
|
[Parameter] public bool Visible { get; set; }
|
|
|
|
/// <summary>Raised when <see cref="Visible"/> changes (supports <c>@bind-Visible</c>).</summary>
|
|
[Parameter] public EventCallback<bool> VisibleChanged { get; set; }
|
|
|
|
/// <summary>The device to export. May be blank when <see cref="TagGroupId"/> is supplied.</summary>
|
|
[Parameter] public string DeviceId { get; set; } = "";
|
|
|
|
/// <summary>When set, only tags under this group's subtree are exported; null exports the whole device.</summary>
|
|
[Parameter] public string? TagGroupId { get; set; }
|
|
|
|
/// <summary>The device's driver-type string — selects the per-driver typed column set.</summary>
|
|
[Parameter] public string DriverType { get; set; } = "";
|
|
|
|
private bool _loading;
|
|
private bool _built;
|
|
private string? _error;
|
|
private string _csv = "";
|
|
private string _dataUri = "";
|
|
private string _fileName = "tags.csv";
|
|
private int _rowCount;
|
|
private string? _groupFilter;
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
if (Visible && !_built)
|
|
{
|
|
_built = true;
|
|
await BuildAsync();
|
|
}
|
|
else if (!Visible)
|
|
{
|
|
_built = false;
|
|
}
|
|
}
|
|
|
|
private async Task BuildAsync()
|
|
{
|
|
_loading = true;
|
|
_error = null;
|
|
try
|
|
{
|
|
var deviceId = DeviceId;
|
|
_groupFilter = null;
|
|
|
|
if (!string.IsNullOrEmpty(TagGroupId))
|
|
{
|
|
var ctx = await ExportReader.ResolveGroupContextAsync(TagGroupId!);
|
|
if (ctx is { } c)
|
|
{
|
|
_groupFilter = c.GroupPath;
|
|
if (string.IsNullOrEmpty(deviceId))
|
|
{
|
|
deviceId = c.DeviceId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(deviceId))
|
|
{
|
|
_error = "Could not resolve the target device.";
|
|
return;
|
|
}
|
|
|
|
var tags = await ExportReader.ReadDeviceTagsAsync(deviceId);
|
|
|
|
// When exporting a single group, keep only that group's subtree (its path + descendants).
|
|
if (!string.IsNullOrEmpty(_groupFilter))
|
|
{
|
|
var prefix = _groupFilter;
|
|
tags = tags
|
|
.Where(t => t.TagGroupPath is not null &&
|
|
(t.TagGroupPath == prefix || t.TagGroupPath.StartsWith(prefix + "/", StringComparison.Ordinal)))
|
|
.ToList();
|
|
}
|
|
|
|
_rowCount = tags.Count;
|
|
_csv = RawTagCsvMapper.Export(DriverType, tags);
|
|
_fileName = BuildFileName(deviceId);
|
|
|
|
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(_csv));
|
|
_dataUri = $"data:text/csv;charset=utf-8;base64,{base64}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_error = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
private static string BuildFileName(string deviceId)
|
|
{
|
|
var safe = new string(deviceId.Select(c => char.IsLetterOrDigit(c) ? c : '-').ToArray());
|
|
return $"tags-{safe}.csv";
|
|
}
|
|
|
|
private async Task Close()
|
|
{
|
|
_built = false;
|
|
_csv = "";
|
|
_dataUri = "";
|
|
Visible = false;
|
|
await VisibleChanged.InvokeAsync(false);
|
|
}
|
|
}
|