feat(central-ui): FlowGraphView log-count badge and exception subtitle on failed nodes (D8)
This commit is contained in:
@@ -40,8 +40,14 @@ public sealed record FlowLayoutNode(
|
||||
/// <summary>Title with <see cref="LanePrefix"/> removed — the discriminating tail of a dotted path.</summary>
|
||||
public string DisplayTitle => FlowGraphView.StripPrefix(Title, LanePrefix) ?? Title;
|
||||
|
||||
/// <summary>Subtitle with <see cref="LanePrefix"/> removed from each comma-separated item.</summary>
|
||||
public string? DisplaySubtitle => FlowGraphView.StripPrefix(Subtitle, LanePrefix);
|
||||
/// <summary>
|
||||
/// Subtitle with <see cref="LanePrefix"/> removed from each comma-separated item — except on a
|
||||
/// failed/timed-out real step, where the exception's type and message take the line instead
|
||||
/// (step-logs design 2026-09-09 D8). A lane prefix is a dotted attribute head, never part of an
|
||||
/// exception line, so the exception text is shown verbatim.
|
||||
/// </summary>
|
||||
public string? DisplaySubtitle => (Step != null ? FlowGraphView.ExceptionSubtitle(Step) : null)
|
||||
?? FlowGraphView.StripPrefix(Subtitle, LanePrefix);
|
||||
|
||||
/// <summary>Full, unclipped text for the hover tooltip.</summary>
|
||||
public string FullText => string.IsNullOrEmpty(Subtitle) ? Title : Title + " — " + Subtitle;
|
||||
@@ -231,6 +237,64 @@ public partial class FlowGraphView : ComponentBase
|
||||
return detail.Length == 0 ? "sync" : detail + " · sync";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log lines on a step folded to per-severity counts for the footer badge (step-logs design
|
||||
/// 2026-09-09 D8). <c>error</c> and <c>warn</c> are counted by name (ordinal-ignore-case);
|
||||
/// everything else — <c>info</c>, <c>debug</c>, an unknown level — counts as info, so the badge
|
||||
/// never silently drops a line.
|
||||
/// </summary>
|
||||
internal static (int Errors, int Warnings, int Info) LogCounts(FlowStep? s)
|
||||
{
|
||||
if (s?.Logs is not { Count: > 0 } lines) return (0, 0, 0);
|
||||
var errors = 0;
|
||||
var warnings = 0;
|
||||
var info = 0;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.Equals(line.Level, "error", StringComparison.OrdinalIgnoreCase)) errors++;
|
||||
else if (string.Equals(line.Level, "warn", StringComparison.OrdinalIgnoreCase)) warnings++;
|
||||
else info++;
|
||||
}
|
||||
|
||||
return (errors, warnings, info);
|
||||
}
|
||||
|
||||
/// <summary>Hover text for the log badge — the non-zero levels only ("1 error, 2 warnings, 4 info").</summary>
|
||||
internal static string LogTitle(FlowLayoutNode n)
|
||||
{
|
||||
var (errors, warnings, info) = LogCounts(n.Step);
|
||||
var parts = new List<string>(3);
|
||||
if (errors > 0) parts.Add($"{errors} error{(errors == 1 ? "" : "s")}");
|
||||
if (warnings > 0) parts.Add($"{warnings} warning{(warnings == 1 ? "" : "s")}");
|
||||
if (info > 0) parts.Add($"{info} info");
|
||||
return string.Join(", ", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The subtitle a failed or timed-out step shows instead of its own: <c>{Type}: {message}</c>, where
|
||||
/// the type is the exception's name without its namespace, parsed from the first line of the captured
|
||||
/// <see cref="FlowStep.ErrorDetail"/> (a .NET <c>ToString()</c> opens with <c>Namespace.Type: message</c>).
|
||||
/// Falls back to the first line verbatim when it carries no <c>": "</c>, and to
|
||||
/// <see cref="FlowStep.ErrorMessage"/> when there is no detail. Null for every non-failed step.
|
||||
/// </summary>
|
||||
internal static string? ExceptionSubtitle(FlowStep s)
|
||||
{
|
||||
if (s.Outcome is not (FlowStepOutcome.Failed or FlowStepOutcome.TimedOut)) return null;
|
||||
if (string.IsNullOrWhiteSpace(s.ErrorDetail)) return s.ErrorMessage;
|
||||
|
||||
var first = s.ErrorDetail.Split('\n', 2)[0].TrimEnd('\r').Trim();
|
||||
if (first.Length == 0) return s.ErrorMessage;
|
||||
|
||||
var split = first.IndexOf(": ", StringComparison.Ordinal);
|
||||
if (split < 0) return first;
|
||||
|
||||
var type = first[..split];
|
||||
var message = first[(split + 2)..];
|
||||
var dot = type.LastIndexOf('.');
|
||||
if (dot >= 0 && dot < type.Length - 1) type = type[(dot + 1)..];
|
||||
return type + ": " + message;
|
||||
}
|
||||
|
||||
private static string Eyebrow(FlowLayoutNode n) => n.Kind switch
|
||||
{
|
||||
FlowStepKind.InboundRequest => "REQUEST",
|
||||
|
||||
Reference in New Issue
Block a user