Merge wt/step-logs-t12: Task 12
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
@using System.Text.Json
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime Js
|
||||
|
||||
@* Flow Debugger step detail (design 2026-09-05 §4): tabs Input · Output ·
|
||||
Attribute writes · Timing plus a "Where this ran" column with drill-ins.
|
||||
@@ -11,7 +12,9 @@
|
||||
System / Connection / List + "Delivered by" facts.
|
||||
Follow-ups (plan 2026-09-06 D2/D5, Task 6): an "Open in …" deep link to the report page
|
||||
for a correlated outbound step, and Retry / Discard buttons on a Parked one when the owner
|
||||
supplies the callbacks. *@
|
||||
supplies the callbacks.
|
||||
Step logs (plan 2026-09-09 D8, Task 12): a Log tab after Output carrying the step's thrown exception
|
||||
and the log lines folded onto it, level-filtered, with a hint to sibling steps of the same execution. *@
|
||||
|
||||
<div class="card" data-test="flow-step-detail">
|
||||
<div class="card-body">
|
||||
@@ -55,10 +58,98 @@
|
||||
case "Output":
|
||||
@if (!string.IsNullOrEmpty(Step.ErrorMessage))
|
||||
{
|
||||
<div class="alert alert-danger small py-1 mb-2" data-test="flow-step-error">@Step.ErrorMessage</div>
|
||||
<div class="alert alert-danger small py-1 mb-2" data-test="flow-step-error">
|
||||
@Step.ErrorMessage
|
||||
@if (HasException)
|
||||
{
|
||||
<text> </text>
|
||||
<a href="#" @onclick="ShowLogTab" @onclick:preventDefault
|
||||
data-test="flow-step-error-see-log">see exception</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<pre class="fsd-json" data-test="flow-step-output">@Pretty(Step.OutputJson)</pre>
|
||||
break;
|
||||
case "Log":
|
||||
@if (HasException)
|
||||
{
|
||||
<div class="fsd-exc" data-test="flow-step-exception">
|
||||
<div class="fsd-exc-type">@ExceptionTypeName</div>
|
||||
<div class="fsd-exc-message">@ExceptionMessageText</div>
|
||||
<pre class="fsd-exc-detail">@Step.ErrorDetail</pre>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm mt-1"
|
||||
data-test="flow-step-exception-copy"
|
||||
@onclick="() => CopyAsync(Step!.ErrorDetail)">Copy</button>
|
||||
</div>
|
||||
}
|
||||
@if (LogLineCount > 0)
|
||||
{
|
||||
<div class="fsd-logctl">
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Log level filter">
|
||||
<button type="button" class="btn @(_warnOnly ? "btn-outline-secondary" : "btn-secondary")"
|
||||
data-test="flow-step-log-filter-all" @onclick="() => _warnOnly = false">All</button>
|
||||
<button type="button" class="btn @(_warnOnly ? "btn-secondary" : "btn-outline-secondary")"
|
||||
data-test="flow-step-log-filter-warn" @onclick="() => _warnOnly = true">Warn +</button>
|
||||
</div>
|
||||
<span class="text-muted">@LogLineCount line@(LogLineCount == 1 ? "" : "s") on this step</span>
|
||||
@if (SiblingStepsWithLogs is { Count: > 0 } siblings)
|
||||
{
|
||||
<span class="text-muted" data-test="flow-step-log-hint">
|
||||
@for (var i = 0; i < siblings.Count; i++)
|
||||
{
|
||||
var other = siblings[i];
|
||||
@(i > 0 ? " · " : "")
|
||||
<text>@other.Logs!.Count more on </text>
|
||||
@if (OnSelectStep.HasDelegate)
|
||||
{
|
||||
<button type="button" class="btn btn-link btn-sm p-0 align-baseline"
|
||||
@onclick="() => OnSelectStep.InvokeAsync(other)">@other.Title</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>@other.Title</text>
|
||||
}
|
||||
}
|
||||
</span>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm ms-auto"
|
||||
data-test="flow-step-log-copy" @onclick="CopyLinesAsync">Copy lines</button>
|
||||
</div>
|
||||
<table class="table table-sm small mb-0" data-test="flow-step-log-lines">
|
||||
<thead><tr><th>Time (UTC)</th><th>Level</th><th>Source</th><th>Message</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach (var line in VisibleLogLines)
|
||||
{
|
||||
<tr data-level="@line.Level">
|
||||
<td class="font-monospace text-nowrap">@line.OccurredAtUtc.ToUniversalTime().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture)</td>
|
||||
<td><span class="fsd-lvl fsd-lvl-@line.Level.ToLowerInvariant()">@line.Level</span></td>
|
||||
<td class="text-muted text-nowrap">@line.Source</td>
|
||||
<td class="text-break">
|
||||
@line.Message
|
||||
@if (!string.IsNullOrEmpty(line.ExceptionMessage))
|
||||
{
|
||||
<div class="fsd-line-exc font-monospace small">
|
||||
@line.ExceptionMessage
|
||||
@if (!string.IsNullOrEmpty(line.ExceptionDetail))
|
||||
{
|
||||
<details>
|
||||
<summary>detail</summary>
|
||||
<pre class="fsd-exc-detail mb-0">@line.ExceptionDetail</pre>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else if (!HasException)
|
||||
{
|
||||
<div class="text-muted small">No log lines on this step.</div>
|
||||
}
|
||||
break;
|
||||
case "Writes":
|
||||
@if (Step.Extra?.Writes is { Count: > 0 } writes)
|
||||
{
|
||||
@@ -187,8 +278,8 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private static readonly string[] Tabs = ["Input", "Output", "Writes", "Timing"];
|
||||
private static readonly string[] TabsWithLifecycle = ["Input", "Output", "Lifecycle", "Timing"];
|
||||
private static readonly string[] Tabs = ["Input", "Output", "Log", "Writes", "Timing"];
|
||||
private static readonly string[] TabsWithLifecycle = ["Input", "Output", "Log", "Lifecycle", "Timing"];
|
||||
|
||||
/// <summary>The selected step; null renders the placeholder.</summary>
|
||||
[Parameter] public FlowStep? Step { get; set; }
|
||||
@@ -211,8 +302,20 @@
|
||||
/// <summary>True while the owner is relaying a Retry / Discard — disables both buttons.</summary>
|
||||
[Parameter] public bool ActionInProgress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Select another step: the Log tab's cross-step hint names the sibling steps of the same execution
|
||||
/// that carry their own lines. No delegate = the hint renders their titles as plain text.
|
||||
/// </summary>
|
||||
[Parameter] public EventCallback<FlowStep> OnSelectStep { get; set; }
|
||||
|
||||
private string _tab = "Input";
|
||||
|
||||
/// <summary>Log level filter: false = All, true = Warn + (hides <c>debug</c> and <c>info</c>).</summary>
|
||||
private bool _warnOnly;
|
||||
|
||||
/// <summary>Step the tab state belongs to — a different step re-picks the default tab and resets the filter.</summary>
|
||||
private Guid? _tabStepId;
|
||||
|
||||
private bool IsOutbound => Step is { } s && FlowStepProjector.IsOutbound(s.Kind);
|
||||
|
||||
/// <summary>
|
||||
@@ -234,19 +337,99 @@
|
||||
private bool ShowActions
|
||||
=> Step is { Outcome: FlowStepOutcome.Parked, CorrelationId: not null } && IsOutbound;
|
||||
|
||||
/// <summary>Outbound steps swap the Writes tab (never populated for them) for the Lifecycle tab.</summary>
|
||||
/// <summary>
|
||||
/// Outbound steps swap the Writes tab (never populated for them) for the Lifecycle tab; the Log tab
|
||||
/// appears only for a step that actually has something to show there (D8).
|
||||
/// </summary>
|
||||
private IEnumerable<string> VisibleTabs
|
||||
=> Step?.Lifecycle is { Count: > 0 } ? TabsWithLifecycle : Tabs;
|
||||
{
|
||||
get
|
||||
{
|
||||
var tabs = Step?.Lifecycle is { Count: > 0 } ? TabsWithLifecycle : Tabs;
|
||||
return HasLogTab ? tabs : tabs.Where(t => t != "Log");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Kind-specific label for the Input tab: what the payload actually is.</summary>
|
||||
/// <summary>Number of log lines folded onto this step.</summary>
|
||||
private int LogLineCount => Step?.Logs?.Count ?? 0;
|
||||
|
||||
/// <summary>True when the step carries a thrown exception's full detail.</summary>
|
||||
private bool HasException => !string.IsNullOrWhiteSpace(Step?.ErrorDetail);
|
||||
|
||||
private bool HasLogTab => LogLineCount > 0 || HasException;
|
||||
|
||||
/// <summary>Kind-specific label for the Input tab, plus the Log tab's line count.</summary>
|
||||
private string TabLabel(string tab)
|
||||
=> tab != "Input" ? tab : Step?.Kind switch
|
||||
{
|
||||
if (tab == "Log") return LogLineCount > 0 ? $"Log ({LogLineCount})" : "Log";
|
||||
if (tab != "Input") return tab;
|
||||
return Step?.Kind switch
|
||||
{
|
||||
FlowStepKind.ApiCall => "Request",
|
||||
FlowStepKind.DbCall => "SQL + parameters",
|
||||
FlowStepKind.Notify => "Subject + body",
|
||||
_ => "Input",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>First line of the exception detail — <c>Namespace.Type: message</c> for a .NET <c>ToString()</c>.</summary>
|
||||
private string FirstDetailLine
|
||||
=> Step?.ErrorDetail is { } d ? d.Split('\n')[0].Trim('\r', ' ') : string.Empty;
|
||||
|
||||
/// <summary>The exception's type, shown in full here (the node badge is the place that shortens it).</summary>
|
||||
private string ExceptionTypeName
|
||||
{
|
||||
get
|
||||
{
|
||||
var line = FirstDetailLine;
|
||||
var i = line.IndexOf(": ", StringComparison.Ordinal);
|
||||
return i > 0 ? line[..i] : line;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The exception's message; falls back to the step's own error summary if the detail is not the usual shape.</summary>
|
||||
private string ExceptionMessageText
|
||||
{
|
||||
get
|
||||
{
|
||||
var line = FirstDetailLine;
|
||||
var i = line.IndexOf(": ", StringComparison.Ordinal);
|
||||
return i > 0 ? line[(i + 2)..] : (Step?.ErrorMessage ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Lines shown under the current filter — <c>Warn +</c> hides <c>debug</c> and <c>info</c>, keeping anything unrecognised.</summary>
|
||||
private IEnumerable<FlowLogLine> VisibleLogLines
|
||||
=> (Step?.Logs ?? []).Where(l => !_warnOnly
|
||||
|| !(string.Equals(l.Level, "debug", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(l.Level, "info", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
/// <summary>Other steps of the same execution that carry lines of their own (the cross-step hint, D8).</summary>
|
||||
private IReadOnlyList<FlowStep> SiblingStepsWithLogs
|
||||
=> Step is { ExecutionId: { } exec } s && Graph is not null
|
||||
? Graph.Steps.Where(o => o.ExecutionId == exec && o.EventId != s.EventId && o.Logs is { Count: > 0 }).ToList()
|
||||
: [];
|
||||
|
||||
private void ShowLogTab() => _tab = "Log";
|
||||
|
||||
/// <summary>Copies the whole exception detail / the shown lines to the clipboard.</summary>
|
||||
private Task CopyLinesAsync()
|
||||
=> CopyAsync(string.Join('\n', VisibleLogLines.Select(l =>
|
||||
$"{l.OccurredAtUtc.ToUniversalTime().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture)} {l.Level} {l.Source} {l.Message}")));
|
||||
|
||||
private async Task CopyAsync(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
try
|
||||
{
|
||||
await Js.InvokeVoidAsync("navigator.clipboard.writeText", text);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Copying is a convenience: the clipboard API is absent outside a secure context and the
|
||||
// interop call throws on a disconnected / prerendering circuit. Never surface that to the operator.
|
||||
}
|
||||
}
|
||||
|
||||
private string TargetLabel => Step?.Kind switch
|
||||
{
|
||||
@@ -264,7 +447,18 @@
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!VisibleTabs.Contains(_tab)) _tab = "Input";
|
||||
if (Step?.EventId != _tabStepId)
|
||||
{
|
||||
// A different step: open on Log when it threw (D8), otherwise keep the operator's current
|
||||
// tab when the new step still has it, and start the level filter over on All.
|
||||
_tabStepId = Step?.EventId;
|
||||
_tab = HasException ? "Log" : (VisibleTabs.Contains(_tab) ? _tab : "Input");
|
||||
_warnOnly = false;
|
||||
}
|
||||
else if (!VisibleTabs.Contains(_tab))
|
||||
{
|
||||
_tab = "Input";
|
||||
}
|
||||
}
|
||||
|
||||
private int AuditRowCount
|
||||
|
||||
+54
@@ -69,3 +69,57 @@
|
||||
max-height: 18rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Step detail Log tab (step-logs plan 2026-09-09 D8): the thrown exception, then the folded lines. */
|
||||
|
||||
::deep .fsd-exc {
|
||||
border: 1px solid var(--bad, var(--bs-danger, #e03131));
|
||||
border-left-width: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--bad-bg, rgba(var(--bs-danger-rgb, 220, 53, 69), 0.08));
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
::deep .fsd-exc-type {
|
||||
font-family: var(--mono, var(--bs-font-monospace));
|
||||
font-weight: 600;
|
||||
color: var(--bad, var(--bs-danger, #e03131));
|
||||
}
|
||||
|
||||
::deep .fsd-exc-detail {
|
||||
margin: 6px 0 0;
|
||||
font-family: var(--mono, var(--bs-font-monospace));
|
||||
font-size: 0.75rem;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
color: var(--ink-soft, var(--bs-secondary-color));
|
||||
}
|
||||
|
||||
::deep .fsd-line-exc {
|
||||
color: var(--ink-soft, var(--bs-secondary-color));
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
::deep .fsd-logctl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
::deep .fsd-lvl {
|
||||
display: inline-block;
|
||||
min-width: 46px;
|
||||
text-align: center;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
::deep .fsd-lvl-info { color: var(--ink-soft, var(--bs-secondary-color)); background: var(--rule, rgba(0, 0, 0, 0.06)); }
|
||||
::deep .fsd-lvl-warn { color: var(--warn, var(--bs-warning, #e8920c)); background: var(--warn-bg, rgba(var(--bs-warning-rgb, 255, 193, 7), 0.15)); }
|
||||
::deep .fsd-lvl-error { color: var(--bad, var(--bs-danger, #e03131)); background: var(--bad-bg, rgba(var(--bs-danger-rgb, 220, 53, 69), 0.12)); }
|
||||
::deep .fsd-lvl-debug { color: var(--ink-faint, var(--bs-tertiary-color)); background: transparent; border: 1px dashed var(--rule-strong, var(--bs-border-color)); }
|
||||
|
||||
@@ -8,11 +8,46 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
|
||||
/// Flow Debugger outbound-calls plan (2026-09-06) Task 8: the step detail panel's Lifecycle tab,
|
||||
/// outbound "Where this ran" fields and kind-specific Input tab labels. Follow-ups plan (2026-09-06)
|
||||
/// Task 6: report deep links from a correlated outbound step and Retry/Discard on a parked one (D2/D5).
|
||||
/// Step-logs plan (2026-09-09) Task 12: the Log tab — exception block, level-filtered lines and the
|
||||
/// cross-step hint (design D8).
|
||||
/// </summary>
|
||||
public class FlowStepDetailTests : BunitContext
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
public FlowStepDetailTests()
|
||||
{
|
||||
// The Log tab's Copy buttons call navigator.clipboard through IJSRuntime.
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
private const string ExceptionDetail =
|
||||
"System.InvalidOperationException: Recipe 42 rejected by Galaxy (code 7)\n" +
|
||||
" at Submission#0.<<Initialize>>d__0.MoveNext() in ProcessRecipeDownload:line 41\n" +
|
||||
" at ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptRunLauncher.RunScriptAsync()";
|
||||
|
||||
/// <summary>A site script-run step, optionally carrying folded log lines and a thrown exception's detail.</summary>
|
||||
private static FlowStep ScriptStep(
|
||||
FlowStepOutcome outcome,
|
||||
string title,
|
||||
Guid executionId,
|
||||
IReadOnlyList<FlowLogLine>? logs = null,
|
||||
string? errorDetail = null)
|
||||
=> new(Guid.NewGuid(), executionId, null, FlowStepKind.ScriptRun, FlowLane.Site, title,
|
||||
"DelmiaSim01.DelmiaReceiver \u00b7 call", T0, 1731, outcome, "{\"RecipeId\":42}", "{\"ok\":false}",
|
||||
outcome == FlowStepOutcome.Failed ? "Recipe 42 rejected by Galaxy (code 7)" : null,
|
||||
"site-a", "node-a", "DelmiaSim01", title, null, null, null, errorDetail, logs);
|
||||
|
||||
private static FlowLogLine Line(
|
||||
string level,
|
||||
string message,
|
||||
string source = "script",
|
||||
int seq = 0,
|
||||
string? exceptionMessage = null,
|
||||
string? exceptionDetail = null)
|
||||
=> new(Guid.NewGuid(), null, T0.AddMilliseconds(seq), level, source, message,
|
||||
exceptionMessage, exceptionDetail, null, seq);
|
||||
|
||||
private static FlowStep Step(FlowStepKind kind, FlowStepOutcome outcome, string target, IReadOnlyList<FlowLifecycleRow>? lifecycle = null, Guid? correlationId = null)
|
||||
=> new(Guid.NewGuid(), Guid.NewGuid(), null, kind, FlowLane.External, target, "Delivered · cached", T0, 4100, outcome,
|
||||
"{\"id\":1}", "{\"ok\":true}", outcome == FlowStepOutcome.Failed ? "boom" : null, "site-a", "node-a", "M1", target, null, lifecycle, correlationId);
|
||||
@@ -182,4 +217,203 @@ public class FlowStepDetailTests : BunitContext
|
||||
Assert.Empty(cut.FindAll("[data-test=flow-step-retry]"));
|
||||
Assert.Empty(cut.FindAll("[data-test=flow-step-discard]"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoLinesAndNoDetail_HidesTheLogTab()
|
||||
{
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, ScriptStep(FlowStepOutcome.Ok, "ProcessRecipeDownload", Guid.NewGuid())));
|
||||
Assert.Empty(cut.FindAll("[data-test=flow-step-tab-log]"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogTab_CountsItsLines()
|
||||
{
|
||||
var step = ScriptStep(FlowStepOutcome.Ok, "ProcessRecipeDownload", Guid.NewGuid(),
|
||||
[
|
||||
Line("info", "Recipe 42 requested", seq: 1),
|
||||
Line("debug", "Writing header attributes", seq: 2),
|
||||
Line("warn", "ResultCode 7 is outside the accepted set", seq: 3),
|
||||
]);
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
||||
|
||||
Assert.Equal("Log (3)", cut.Find("[data-test=flow-step-tab-log]").TextContent.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedStepWithDetail_OpensOnLog_AndRendersTheExceptionBlock()
|
||||
{
|
||||
var step = ScriptStep(FlowStepOutcome.Failed, "ProcessRecipeDownload", Guid.NewGuid(),
|
||||
[Line("error", "Rejecting recipe 42 (code 7)", seq: 1)], ExceptionDetail);
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
||||
|
||||
Assert.Contains("active", cut.Find("[data-test=flow-step-tab-log]").ClassName);
|
||||
var exc = cut.Find("[data-test=flow-step-exception]");
|
||||
Assert.Equal("System.InvalidOperationException", exc.QuerySelector(".fsd-exc-type")!.TextContent.Trim());
|
||||
Assert.Equal("Recipe 42 rejected by Galaxy (code 7)", exc.QuerySelector(".fsd-exc-message")!.TextContent.Trim());
|
||||
Assert.Contains("ScriptRunLauncher.RunScriptAsync", exc.QuerySelector("pre")!.TextContent);
|
||||
Assert.NotNull(cut.Find("[data-test=flow-step-exception-copy]"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OkStepWithLines_OpensOnInput()
|
||||
{
|
||||
var step = ScriptStep(FlowStepOutcome.Ok, "ProcessRecipeDownload", Guid.NewGuid(), [Line("info", "hello", seq: 1)]);
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
||||
|
||||
Assert.Contains("active", cut.Find("[data-test=flow-step-tab-input]").ClassName);
|
||||
Assert.DoesNotContain("active", cut.Find("[data-test=flow-step-tab-log]").ClassName);
|
||||
Assert.Empty(cut.FindAll("[data-test=flow-step-exception]"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarnFilter_LeavesOnlyWarnAndErrorRows()
|
||||
{
|
||||
var step = ScriptStep(FlowStepOutcome.Ok, "ProcessRecipeDownload", Guid.NewGuid(),
|
||||
[
|
||||
Line("info", "Recipe 42 requested", seq: 1),
|
||||
Line("debug", "Writing header attributes", seq: 2),
|
||||
Line("warn", "ResultCode 7 is outside the accepted set", seq: 3),
|
||||
Line("error", "Rejecting recipe 42", seq: 4),
|
||||
]);
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
||||
cut.Find("[data-test=flow-step-tab-log]").Click();
|
||||
|
||||
Assert.Equal(4, cut.FindAll("[data-test=flow-step-log-lines] tr[data-level]").Count);
|
||||
|
||||
cut.Find("[data-test=flow-step-log-filter-warn]").Click();
|
||||
var rows = cut.FindAll("[data-test=flow-step-log-lines] tr[data-level]");
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.Equal("warn", rows[0].GetAttribute("data-level"));
|
||||
Assert.Equal("error", rows[1].GetAttribute("data-level"));
|
||||
|
||||
cut.Find("[data-test=flow-step-log-filter-all]").Click();
|
||||
Assert.Equal(4, cut.FindAll("[data-test=flow-step-log-lines] tr[data-level]").Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hint_NamesAnotherStepOfTheSameExecution_AndSelectsIt()
|
||||
{
|
||||
var exec = Guid.NewGuid();
|
||||
var selected = ScriptStep(FlowStepOutcome.Ok, "ProcessRecipeDownload", exec, [Line("info", "a", seq: 1)]);
|
||||
var other = ScriptStep(FlowStepOutcome.Ok, "Write RecipeId, RecipeName", exec, [Line("warn", "acked Uncertain", seq: 2)]);
|
||||
var elsewhere = ScriptStep(FlowStepOutcome.Ok, "Another execution", Guid.NewGuid(), [Line("info", "c", seq: 3)]);
|
||||
var graph = new FlowGraph([selected, other, elsewhere], [], false, false);
|
||||
FlowStep? picked = null;
|
||||
|
||||
var cut = Render<FlowStepDetail>(p => p
|
||||
.Add(x => x.Step, selected)
|
||||
.Add(x => x.Graph, graph)
|
||||
.Add(x => x.OnSelectStep, s => picked = s));
|
||||
cut.Find("[data-test=flow-step-tab-log]").Click();
|
||||
|
||||
var hint = cut.Find("[data-test=flow-step-log-hint]");
|
||||
Assert.Contains("1 more on", hint.TextContent);
|
||||
Assert.Contains("Write RecipeId, RecipeName", hint.TextContent);
|
||||
Assert.DoesNotContain("Another execution", hint.TextContent);
|
||||
|
||||
hint.QuerySelector("button")!.Click();
|
||||
Assert.Same(other, picked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hint_WithoutASelectCallback_IsPlainText()
|
||||
{
|
||||
var exec = Guid.NewGuid();
|
||||
var selected = ScriptStep(FlowStepOutcome.Ok, "ProcessRecipeDownload", exec, [Line("info", "a", seq: 1)]);
|
||||
var other = ScriptStep(FlowStepOutcome.Ok, "Write RecipeId, RecipeName", exec, [Line("warn", "b", seq: 2)]);
|
||||
var graph = new FlowGraph([selected, other], [], false, false);
|
||||
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, selected).Add(x => x.Graph, graph));
|
||||
cut.Find("[data-test=flow-step-tab-log]").Click();
|
||||
|
||||
var hint = cut.Find("[data-test=flow-step-log-hint]");
|
||||
Assert.Contains("Write RecipeId, RecipeName", hint.TextContent);
|
||||
Assert.Null(hint.QuerySelector("button"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputAlert_SeeExceptionLink_SwitchesToTheLogTab()
|
||||
{
|
||||
var step = ScriptStep(FlowStepOutcome.Failed, "ProcessRecipeDownload", Guid.NewGuid(),
|
||||
[Line("error", "Rejecting recipe 42", seq: 1)], ExceptionDetail);
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
||||
|
||||
cut.Find("[data-test=flow-step-tab-output]").Click();
|
||||
Assert.Contains("Recipe 42 rejected", cut.Find("[data-test=flow-step-error]").TextContent);
|
||||
|
||||
cut.Find("[data-test=flow-step-error-see-log]").Click();
|
||||
Assert.Contains("active", cut.Find("[data-test=flow-step-tab-log]").ClassName);
|
||||
Assert.NotNull(cut.Find("[data-test=flow-step-exception]"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeSourcedLine_ShowsItsSource_AndAnAttachedException()
|
||||
{
|
||||
var step = ScriptStep(FlowStepOutcome.Ok, "Write RecipeId", Guid.NewGuid(),
|
||||
[
|
||||
Line("warn", "acked with quality Uncertain", source: "runtime", seq: 1,
|
||||
exceptionMessage: "Tag write was not confirmed", exceptionDetail: "System.TimeoutException: Tag write was not confirmed\n at Dcl"),
|
||||
]);
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
||||
cut.Find("[data-test=flow-step-tab-log]").Click();
|
||||
|
||||
var row = cut.Find("[data-test=flow-step-log-lines] tr[data-level=warn]");
|
||||
Assert.Contains("runtime", row.TextContent);
|
||||
Assert.Contains("Tag write was not confirmed", row.QuerySelector(".fsd-line-exc")!.TextContent);
|
||||
Assert.Contains("System.TimeoutException", row.QuerySelector("details pre")!.TextContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SwitchingSteps_KeepsAStillValidTab()
|
||||
{
|
||||
var a = ScriptStep(FlowStepOutcome.Ok, "ProcessRecipeDownload", Guid.NewGuid());
|
||||
var b = ScriptStep(FlowStepOutcome.Ok, "AckRecipe", Guid.NewGuid());
|
||||
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, a));
|
||||
cut.Find("[data-test=flow-step-tab-timing]").Click();
|
||||
Assert.Contains("active", cut.Find("[data-test=flow-step-tab-timing]").ClassName);
|
||||
|
||||
cut.Render(p => p.Add(x => x.Step, b));
|
||||
|
||||
Assert.Contains("active", cut.Find("[data-test=flow-step-tab-timing]").ClassName);
|
||||
Assert.DoesNotContain("active", cut.Find("[data-test=flow-step-tab-input]").ClassName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefreshingTheSameStep_KeepsTheTabAndTheLevelFilter()
|
||||
{
|
||||
// The page re-reads outcomes after a Live run and hands back a NEW FlowStep instance carrying
|
||||
// the same EventId plus whatever rows have landed since — that must not disturb the panel.
|
||||
var eventId = Guid.NewGuid();
|
||||
var exec = Guid.NewGuid();
|
||||
FlowStep Refreshed(IReadOnlyList<FlowLogLine> logs)
|
||||
=> new(eventId, exec, null, FlowStepKind.ScriptRun, FlowLane.Site, "ProcessRecipeDownload",
|
||||
"sub", T0, 1731, FlowStepOutcome.Failed, "{}", "{}", "Recipe 42 rejected by Galaxy (code 7)",
|
||||
"site-a", "node-a", "DelmiaSim01", "ProcessRecipeDownload", null, null, null, ExceptionDetail, logs);
|
||||
|
||||
var first = Refreshed([Line("info", "Recipe 42 requested", seq: 1), Line("warn", "ResultCode 7", seq: 2)]);
|
||||
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, first));
|
||||
|
||||
Assert.Equal("Log (2)", cut.Find("[data-test=flow-step-tab-log]").TextContent.Trim());
|
||||
cut.Find("[data-test=flow-step-log-filter-warn]").Click();
|
||||
Assert.Single(cut.FindAll("[data-test=flow-step-log-lines] tr[data-level]"));
|
||||
cut.Find("[data-test=flow-step-tab-timing]").Click();
|
||||
|
||||
cut.Render(p => p.Add(x => x.Step, Refreshed(
|
||||
[
|
||||
Line("info", "Recipe 42 requested", seq: 1),
|
||||
Line("warn", "ResultCode 7", seq: 2),
|
||||
Line("error", "Rejecting recipe 42", seq: 3),
|
||||
])));
|
||||
|
||||
Assert.Contains("active", cut.Find("[data-test=flow-step-tab-timing]").ClassName);
|
||||
Assert.Equal("Log (3)", cut.Find("[data-test=flow-step-tab-log]").TextContent.Trim());
|
||||
|
||||
cut.Find("[data-test=flow-step-tab-log]").Click();
|
||||
Assert.Contains("btn-secondary", cut.Find("[data-test=flow-step-log-filter-warn]").ClassName);
|
||||
var rows = cut.FindAll("[data-test=flow-step-log-lines] tr[data-level]");
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.Equal("warn", rows[0].GetAttribute("data-level"));
|
||||
Assert.Equal("error", rows[1].GetAttribute("data-level"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user