Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor
T

129 lines
7.3 KiB
Plaintext

@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging
@using ZB.MOM.WW.ScadaBridge.ScriptAnalysis
@* Flow Debugger swimlane (design 2026-09-05 §4; External lane, status pills and async/spawn
connectors per the 2026-09-06 outbound-calls plan). Dependency-free inline SVG: four fixed
lanes, one node per step, orthogonal connectors. All coordinate math lives in the
code-behind (InvariantCulture throughout). *@
<div class="fg-wrap" data-test="flow-graph">
<svg width="@RenderedWidth" height="@RenderedHeight" viewBox="@ViewBox"
role="img" aria-label="@AriaSummary" class="fg-svg">
<defs>
<marker id="fg-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" class="fg-arrowhead" />
</marker>
<marker id="fg-arrow-accent" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" class="fg-arrowhead accent" />
</marker>
</defs>
@* Lanes *@
@for (var lane = 0; lane < LaneCount; lane++)
{
var i = lane;
<g class="fg-lane" data-lane="@LaneName(i)">
<rect x="0" y="@LaneY(i)" width="@F(_layout.Width)" height="@F(LaneHeight)" class="fg-lane-bg @(i % 2 == 1 ? "alt" : "")" />
<line x1="0" y1="@LaneY(i)" x2="@F(_layout.Width)" y2="@LaneY(i)" class="fg-lane-rule" />
@SvgText("12", LaneLabelY(i), "fg-lane-label", LaneName(i))
@if (!string.IsNullOrEmpty(_layout.LaneSublabels[i]))
{
@SvgText("12", F(i * LaneHeight + 40), "fg-lane-sublabel", Clip(_layout.LaneSublabels[i], 22), title: _layout.LaneSublabels[i])
}
@if (!string.IsNullOrEmpty(_layout.LanePrefixes[i]))
{
@* The dotted head every node in this lane shares — shown here once, stripped from the nodes. *@
@SvgText("12", F(i * LaneHeight + 56), "fg-lane-prefix", Clip(_layout.LanePrefixes[i] + "*", 22), title: _layout.LanePrefixes[i] + "* (prefix shared by every node in this lane)", dataTest: "flow-lane-prefix")
}
</g>
}
@if (_layout.IsEmpty)
{
@SvgText(F(ColumnOrigin), F(LanePad + 44), "fg-empty", "No steps", dataTest: "flow-graph-empty")
}
@* Connectors (under the nodes) *@
@foreach (var e in _layout.Edges)
{
<path d="@e.Path" class="@EdgeClass(e)" marker-end="@MarkerFor(e)" data-test="flow-edge" data-from="@e.From" data-to="@e.To" />
@if (AsyncTagFor(e) is { } tag)
{
@* Names the branch where it lands: the run carried on without waiting for this. *@
@SvgText(tag.X, tag.Y, "fg-edge-tag", "async", dataTest: "flow-edge-tag")
}
@if (SpawnBadgeFor(e) is { } badge)
{
@* Trigger cascade (plan D9): a bolt badge on the connector itself — this write fired a value-change
script. The edge keeps an ordinary arrowhead; the bolt used to BE the arrowhead, and on the
upward Tags→Site hop it read as a broken arrow (rig, 2026-09-08). *@
<g class="fg-spawn-badge" transform="translate(@badge.X @badge.Y)" data-test="flow-edge-spawn">
<title>Trigger cascade: this write fired the script</title>
<circle r="8" class="fg-spawn-badge-bg" />
<path d="M -1 -6 l -4 6 h 5 l -3 6 l 7 -8 h -5 l 3 -4 z" class="fg-spawn-bolt" />
</g>
}
}
@* Nodes *@
@foreach (var n in _layout.Nodes)
{
var node = n;
var logs = LogCounts(node.Step);
<g class="@NodeClass(node)" transform="translate(@F(node.X) @F(node.Y))"
role="button" tabindex="0"
aria-label="@(Eyebrow(node) + ": " + node.Title)"
data-test="flow-node" data-key="@node.Key" data-kind="@node.Kind" data-lane="@node.Lane" data-column="@node.Column"
@onclick="() => SelectAsync(node)" @onkeydown="e => OnKeyAsync(e, node)">
<rect x="0" y="0" width="@F(NodeWidth)" height="@F(NodeHeight)" rx="4" ry="4" class="fg-node-box" />
@* HTML body inside the SVG so the browser measures the text: the title wraps to two
lines and both lines ellipsize by real width, never by a character count, and
nothing can spill past the box. The tooltip carries the full, unstripped text. *@
<foreignObject x="0" y="0" width="@F(NodeWidth)" height="@F(NodeHeight)">
<div xmlns="http://www.w3.org/1999/xhtml" class="fg-body" title="@node.FullText" data-test="flow-node-body">
<div class="fg-head">
<span class="fg-eyebrow">@Eyebrow(node)</span>
<span class="fg-duration">@Duration(node)</span>
</div>
<div class="fg-title" data-test="flow-node-title">@WrapHints(node.DisplayTitle)</div>
@* Footer row: the pill-or-subtitle on the left, the log-count badge on the right
(step-logs design 2026-09-09 D8). A node with no lines shows no badge at all. *@
<div class="fg-foot">
@if (HasPill(node))
{
@* Outbound kinds: the folded outcome as a pill, then the call mode (node anatomy: "● Ok · sync"). *@
<div class="fg-status" data-test="flow-node-status">
<span class="@PillClass(node.Step!.Outcome)" data-test="flow-node-pill">@PillText(node.Step.Outcome)</span>
<span class="fg-mode">· @OutboundMode(node.Kind, node.Subtitle)</span>
</div>
}
else
{
<div class="fg-subtitle" data-test="flow-node-subtitle">@node.DisplaySubtitle</div>
}
@if (logs.Errors + logs.Warnings + logs.Info > 0)
{
<span class="fg-logs" data-test="flow-node-logs" title="@LogTitle(logs)">
@if (logs.Errors > 0)
{
<i class="e">✕ @logs.Errors</i>
}
@if (logs.Warnings > 0)
{
<i class="w">! @logs.Warnings</i>
}
@if (logs.Info > 0)
{
<i class="i">≡ @logs.Info</i>
}
</span>
}
</div>
</div>
</foreignObject>
</g>
}
</svg>
</div>