fix(adminui): one save-error area per driver/device modal; refusal chips print the full message (#847)
This commit is contained in:
@@ -62,14 +62,7 @@
|
||||
<DriverTestConnectButton DriverType="@_driverType" GetConfigJson="@BuildMergedProbeConfig" />
|
||||
</div>
|
||||
|
||||
@if (DriverConfigSaveGate.Validate(_driverType, _deviceConfigJson) is { } gateError)
|
||||
{
|
||||
<div class="panel notice notice-alert mt-3">@gateError</div>
|
||||
}
|
||||
@if (_saveError is not null)
|
||||
{
|
||||
<div class="panel notice notice-alert mt-3">@_saveError</div>
|
||||
}
|
||||
<SaveRefusalNotice GateError="@DriverConfigSaveGate.Validate(_driverType, _deviceConfigJson)" SaveError="@_saveError" />
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -122,7 +115,7 @@
|
||||
{
|
||||
["DeviceConfigJson"] = _deviceConfigJson,
|
||||
["DeviceConfigJsonChanged"] = _deviceConfigJsonChanged ??=
|
||||
EventCallback.Factory.Create<string>(this, v => _deviceConfigJson = v),
|
||||
EventCallback.Factory.Create<string>(this, v => { _deviceConfigJson = v; _saveError = null; }),
|
||||
});
|
||||
private readonly ParameterDictionaryCache _deviceFormParameterCache = new();
|
||||
private EventCallback<string>? _deviceConfigJsonChanged;
|
||||
@@ -200,8 +193,10 @@
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_saveError = DriverConfigSaveGate.Validate(_driverType, _deviceConfigJson);
|
||||
if (_saveError is not null) return;
|
||||
// A resubmit REPLACES the previous refusal; a gate refusal is already rendered live by
|
||||
// SaveRefusalNotice, so it is not copied into _saveError (that stacked two boxes — lmxopcua#847).
|
||||
_saveError = null;
|
||||
if (DriverConfigSaveGate.Validate(_driverType, _deviceConfigJson) is not null) return;
|
||||
_busy = true;
|
||||
try
|
||||
{
|
||||
|
||||
+6
-11
@@ -63,14 +63,7 @@
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (DriverConfigSaveGate.Validate(_driverType, _driverConfigJson) is { } gateError)
|
||||
{
|
||||
<div class="panel notice notice-alert mt-3">@gateError</div>
|
||||
}
|
||||
@if (_saveError is not null)
|
||||
{
|
||||
<div class="panel notice notice-alert mt-3">@_saveError</div>
|
||||
}
|
||||
<SaveRefusalNotice GateError="@DriverConfigSaveGate.Validate(_driverType, _driverConfigJson)" SaveError="@_saveError" />
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -126,7 +119,7 @@
|
||||
{
|
||||
["DriverConfigJson"] = _driverConfigJson,
|
||||
["DriverConfigJsonChanged"] = _driverConfigJsonChanged ??=
|
||||
EventCallback.Factory.Create<string>(this, v => _driverConfigJson = v),
|
||||
EventCallback.Factory.Create<string>(this, v => { _driverConfigJson = v; _saveError = null; }),
|
||||
["ResilienceConfig"] = _resilienceConfig!,
|
||||
["ResilienceConfigChanged"] = _resilienceConfigChanged ??=
|
||||
EventCallback.Factory.Create<string?>(this, v => _resilienceConfig = v),
|
||||
@@ -190,8 +183,10 @@
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_saveError = DriverConfigSaveGate.Validate(_driverType, _driverConfigJson);
|
||||
if (_saveError is not null) return;
|
||||
// A resubmit REPLACES the previous refusal; a gate refusal is already rendered live by
|
||||
// SaveRefusalNotice, so it is not copied into _saveError (that stacked two boxes — lmxopcua#847).
|
||||
_saveError = null;
|
||||
if (DriverConfigSaveGate.Validate(_driverType, _driverConfigJson) is not null) return;
|
||||
_busy = true;
|
||||
try
|
||||
{
|
||||
|
||||
+1
-3
@@ -29,9 +29,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="chip chip-bad" title="@_result.Message">
|
||||
Failed · @DisplayText.Abbreviate(_result.Message, 60)
|
||||
</span>
|
||||
<RefusalChip Message="@_result.Message" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
+1
-3
@@ -57,7 +57,7 @@
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (_openError is not null) { <span class="chip chip-bad" title="@_openError">@TruncatedError()</span> }
|
||||
@if (_openError is not null) { <RefusalChip Message="@_openError" /> }
|
||||
</div>
|
||||
|
||||
@if (_token != Guid.Empty)
|
||||
@@ -154,8 +154,6 @@
|
||||
return _useIndex ? $"{_tagName}[{_elementIndex}]" : _tagName;
|
||||
}
|
||||
|
||||
private string TruncatedError() =>
|
||||
_openError is null ? "" : (_openError.Length > 60 ? _openError[..60] + "…" : _openError);
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
|
||||
+1
-3
@@ -121,7 +121,7 @@
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (_openError is not null) { <span class="chip chip-bad" title="@_openError">@TruncatedError()</span> }
|
||||
@if (_openError is not null) { <RefusalChip Message="@_openError" /> }
|
||||
</div>
|
||||
|
||||
@if (_token != Guid.Empty)
|
||||
@@ -228,8 +228,6 @@
|
||||
? FocasAddressBuilder.BuildAlarm(_alarmScope, _alarmLow, _alarmHigh)
|
||||
: FocasAddressBuilder.Build(_kind, _pmcLetter, _number, _bitIndex);
|
||||
|
||||
private string TruncatedError() =>
|
||||
_openError is null ? "" : (_openError.Length > 60 ? _openError[..60] + "…" : _openError);
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
|
||||
+1
-3
@@ -42,7 +42,7 @@
|
||||
<span class="chip chip-ok">Browser open</span>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="CloseBrowseAsync">Close</button>
|
||||
}
|
||||
@if (_openError is not null) { <span class="chip chip-bad" title="@_openError">@TruncatedError()</span> }
|
||||
@if (_openError is not null) { <RefusalChip Message="@_openError" /> }
|
||||
</div>
|
||||
|
||||
@if (_token != Guid.Empty)
|
||||
@@ -215,8 +215,6 @@
|
||||
return $"{_tagName}.{_attributeName}";
|
||||
}
|
||||
|
||||
private string TruncatedError() =>
|
||||
_openError is null ? "" : (_openError.Length > 60 ? _openError[..60] + "…" : _openError);
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
|
||||
+1
-3
@@ -40,7 +40,7 @@
|
||||
Close
|
||||
</button>
|
||||
}
|
||||
@if (_openError is not null) { <span class="chip chip-bad" title="@_openError">@TruncatedError()</span> }
|
||||
@if (_openError is not null) { <RefusalChip Message="@_openError" /> }
|
||||
</div>
|
||||
|
||||
@if (_token != Guid.Empty)
|
||||
@@ -113,8 +113,6 @@
|
||||
await CurrentAddressChanged.InvokeAsync(_built);
|
||||
}
|
||||
|
||||
private string TruncatedError() =>
|
||||
_openError is null ? "" : (_openError.Length > 60 ? _openError[..60] + "…" : _openError);
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
|
||||
+1
-1
@@ -156,7 +156,7 @@ else
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="CloseBrowseAsync">Close</button>
|
||||
}
|
||||
@* Same open-error chip the five sibling pickers use: abbreviated inline, full text on title. *@
|
||||
@if (_openError is not null) { <span class="chip chip-bad" title="@_openError">@DisplayText.Abbreviate(_openError, 60)</span> }
|
||||
@if (_openError is not null) { <RefusalChip Message="@_openError" /> }
|
||||
</div>
|
||||
|
||||
@if (_token != Guid.Empty)
|
||||
|
||||
+1
-3
@@ -46,7 +46,7 @@
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (_openError is not null) { <span class="chip chip-bad" title="@_openError">@TruncatedError()</span> }
|
||||
@if (_openError is not null) { <RefusalChip Message="@_openError" /> }
|
||||
</div>
|
||||
|
||||
@if (_token != Guid.Empty)
|
||||
@@ -133,8 +133,6 @@
|
||||
await CurrentAddressChanged.InvokeAsync(_built);
|
||||
}
|
||||
|
||||
private string TruncatedError() =>
|
||||
_openError is null ? "" : (_openError.Length > 60 ? _openError[..60] + "…" : _openError);
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
@* A failed Test Connect / browse-open outcome (lmxopcua#847). The chip carries only a short status word; the
|
||||
full refusal text is rendered READABLY under it — wrapping, normal case, selectable — instead of being cut
|
||||
to a few characters inside the (uppercase, inline) chip with the real reason only in a hover tooltip. A
|
||||
secret-reference refusal ("SEC…") was unreadable that way, and a tooltip cannot be copied from.
|
||||
Renders as an inline column, so it drops into the existing d-flex rows beside the button unchanged. *@
|
||||
<span class="d-inline-flex flex-column align-items-start" style="min-width:0;max-width:100%">
|
||||
<span class="chip chip-bad">@Label</span>
|
||||
@if (!string.IsNullOrWhiteSpace(Message))
|
||||
{
|
||||
<span class="small text-danger text-break mt-1" style="white-space:pre-wrap;user-select:text">@Message</span>
|
||||
}
|
||||
</span>
|
||||
|
||||
@code {
|
||||
/// <summary>The short chip text (default <c>Failed</c>).</summary>
|
||||
[Parameter] public string Label { get; set; } = "Failed";
|
||||
|
||||
/// <summary>The full refusal / failure message, shown untruncated under the chip.</summary>
|
||||
[Parameter] public string? Message { get; set; }
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
@* The single save-error area under a driver/device form (lmxopcua#847) — see SaveRefusalNotices. *@
|
||||
@foreach (var message in SaveRefusalNotices.Messages(GateError, SaveError))
|
||||
{
|
||||
<div class="panel notice notice-alert mt-3" role="alert">@message</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>The live save-gate refusal for the current form state, or null.</summary>
|
||||
[Parameter] public string? GateError { get; set; }
|
||||
|
||||
/// <summary>The last save attempt's error, or null.</summary>
|
||||
[Parameter] public string? SaveError { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;
|
||||
|
||||
/// <summary>
|
||||
/// The one error area a driver/device modal shows under its form (lmxopcua#847). The modals used to render the
|
||||
/// live <see cref="Uns.DriverConfigSaveGate"/> refusal AND the last save's error as two independent boxes, and
|
||||
/// a Save that the gate refused copied the gate text into the save error — so a refused save (e.g. a Galaxy
|
||||
/// credential reference outside its namespace) stacked two identical boxes, and a server refusal from an
|
||||
/// earlier save could stay standing beside a newer one.
|
||||
/// </summary>
|
||||
public static class SaveRefusalNotices
|
||||
{
|
||||
/// <summary>The distinct messages to render, the live gate refusal first.</summary>
|
||||
/// <param name="gateError">The live save-gate refusal for the current form state, or null.</param>
|
||||
/// <param name="saveError">The last save attempt's error, or null.</param>
|
||||
/// <returns>Zero, one or two messages — never the same text twice, and never a save error that merely
|
||||
/// repeats (contains, or is contained by) the gate refusal already shown.</returns>
|
||||
public static IReadOnlyList<string> Messages(string? gateError, string? saveError)
|
||||
{
|
||||
var gate = string.IsNullOrWhiteSpace(gateError) ? null : gateError.Trim();
|
||||
var save = string.IsNullOrWhiteSpace(saveError) ? null : saveError.Trim();
|
||||
|
||||
if (gate is null)
|
||||
{
|
||||
return save is null ? [] : [save];
|
||||
}
|
||||
|
||||
if (save is null
|
||||
|| save.Contains(gate, StringComparison.Ordinal)
|
||||
|| gate.Contains(save, StringComparison.Ordinal))
|
||||
{
|
||||
return [gate];
|
||||
}
|
||||
|
||||
return [gate, save];
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@
|
||||
</button>
|
||||
@if (_openError is not null)
|
||||
{
|
||||
<span class="chip chip-bad" title="@_openError">@Truncate(_openError, 70)</span>
|
||||
<RefusalChip Message="@_openError" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -845,7 +845,6 @@
|
||||
private string TargetLabel()
|
||||
=> string.IsNullOrEmpty(_groupPrefix) ? "(device root)" : _groupPrefix!;
|
||||
|
||||
private static string Truncate(string s, int max) => s.Length > max ? s[..max] + "…" : s;
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user