47d148daf9
`Scripts.razor` rendered a hash prefix as `@s.SourceHash[..12]…`. `SourceHash`
is a plain nvarchar(64) with no length floor, so any value shorter than 12
characters threw ArgumentOutOfRangeException out of BuildRenderTree — unhandled
in Blazor, which takes the WHOLE page to an HTTP 500, not just the offending
row. Live-reproduced on docker-dev, where two Script rows carry `h1` and
`h-abs-hr200`: /scripts was a hard 500.
New `Components/Shared/DisplayText.Abbreviate(string?, int)`:
- null/blank renders the Admin UI's em-dash placeholder rather than throwing;
- a value already within budget renders verbatim with NO ellipsis, so the
display never implies text that isn't there (the old markup appended "…"
unconditionally, outside the slice — `h1` would have shown as `h1…`);
- a maxLength < 1 is clamped, so a display bug cannot become a page crash.
Applied to every unguarded range-operator slice over a DB-sourced string found
by sweeping the AdminUI for `[..N]`:
Scripts.razor SourceHash (the reported crash)
Certificates.razor Thumbprint (read off the on-disk store)
Deployments.razor x2 RevisionHash
Clusters/ClusterOverview.razor RevisionHash
The other `[..N]` hits are safe and untouched: Guid.ToString("N") slices
(always 32 chars) and the `Length > 60 ? [..60]` ternaries that self-guard.
NOTE — the issue text blamed the docker-dev seed; that was wrong and is
corrected on #504. `seed-clusters.sql` inserts only ServerCluster, ClusterNode
and LdapGroupRoleMapping. The short-hash rows came from earlier live-gate
authoring. A freshly seeded rig is fine; the trigger is any Script row that did
not go through ScriptEdit / UnsTreeService.HashSource — REST-API authored,
hand-inserted, or migrated in.
Tests: 14 new in DisplayTextTests, covering the short/null/blank/clamped cases
and a never-throws sweep over every value x budget combination. AdminUI suite
739/739.
Live-verified on docker-dev (AdminUI has no bUnit): /scripts 500 -> 200 across
both central nodes, rendering `hash=h1` and `hash=h-abs-hr200` in full;
/deployments still truncates 64-char hashes at 12 with the ellipsis;
/certificates and /clusters/MAIN unchanged.
46 lines
2.5 KiB
C#
46 lines
2.5 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared;
|
|
|
|
/// <summary>
|
|
/// Rendering helpers for operator-facing text that comes out of the database.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Why this exists (#504).</b> Several pages abbreviated a hash / thumbprint with the range
|
|
/// operator — <c>@s.SourceHash[..12]…</c> — which throws
|
|
/// <see cref="ArgumentOutOfRangeException"/> the moment the value is shorter than the slice.
|
|
/// In Blazor that escapes <c>BuildRenderTree</c> unhandled and takes the <b>whole page</b> to an
|
|
/// HTTP 500, not just the offending row. Nothing enforces a minimum length at the schema, the
|
|
/// entity, or the render layer, so any hand-written / seeded / migrated row is a live page-kill.
|
|
/// The docker-dev seed proved it: its placeholder <c>SourceHash</c> values (<c>h1</c>,
|
|
/// <c>h-abs-hr200</c>) made <c>/scripts</c> a hard 500 on every stock bring-up.
|
|
/// </para>
|
|
/// <para>
|
|
/// Use <see cref="Abbreviate"/> for <b>every</b> such prefix. It never throws, and it appends the
|
|
/// ellipsis only when it actually truncated — a 2-character hash renders as <c>h1</c>, not
|
|
/// <c>h1…</c>, so the display never implies more text than exists.
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class DisplayText
|
|
{
|
|
/// <summary>Placeholder rendered for a null / blank value. Matches the em-dash convention the
|
|
/// Admin UI already uses for "nothing to show" cells.</summary>
|
|
public const string Empty = "—";
|
|
|
|
/// <summary>
|
|
/// The first <paramref name="maxLength"/> characters of <paramref name="value"/>, followed by an
|
|
/// ellipsis when (and only when) characters were dropped.
|
|
/// </summary>
|
|
/// <param name="value">The value to abbreviate. May be null, blank, or shorter than
|
|
/// <paramref name="maxLength"/> — none of which throw.</param>
|
|
/// <param name="maxLength">Maximum number of characters to keep. Values below 1 are treated as 1,
|
|
/// so a caller cannot turn a display bug into a crash.</param>
|
|
/// <returns><see cref="Empty"/> for a null/blank value; the value verbatim when it already fits;
|
|
/// otherwise the truncated prefix plus an ellipsis.</returns>
|
|
public static string Abbreviate(string? value, int maxLength)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value)) return Empty;
|
|
if (maxLength < 1) maxLength = 1;
|
|
return value.Length <= maxLength ? value : string.Concat(value.AsSpan(0, maxLength), "…");
|
|
}
|
|
}
|