feat(central-ui): FlowGraphView SVG swimlane renderer
Dependency-free inline SVG: three fixed 110px lanes (Central / Site / Tags, sublabelled from the steps' site/instance), columns from a topological pass over the graph edges with same-lane collision bumping so independent siblings never overlap, 150x78 nodes on a 130+col*170 grid, orthogonal connectors with arrow markers (accent for boundary crossings, dashed for external actors, light for predicted edges). Renders a real FlowGraph, a PredictedFlow (dashed nodes, input-dependent finer-dashed), or both — predicted nodes matched to real steps by kind in order, the leftovers appended greyed as ghosts. Selected node = accent 2px on --active-bg; failed/timed-out/not-traced outcomes styled; each node a role=button tabindex=0 <g> with click + Enter/Space selection; svg role=img with a summary aria-label. Layout is a pure static ComputeLayout, every coordinate InvariantCulture. Razor reserves the <text> tag, so SVG text elements are emitted through a small HTML-encoding MarkupString helper. Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
@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). Dependency-free inline SVG:
|
||||
three 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 < 3; 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], 16), title: _layout.LaneSublabels[i])
|
||||
}
|
||||
</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" />
|
||||
}
|
||||
|
||||
@* Nodes *@
|
||||
@foreach (var n in _layout.Nodes)
|
||||
{
|
||||
var node = n;
|
||||
<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)">
|
||||
<title>@node.Title@(string.IsNullOrEmpty(node.Subtitle) ? "" : " — " + node.Subtitle)</title>
|
||||
<rect x="0" y="0" width="@F(NodeWidth)" height="@F(NodeHeight)" rx="4" ry="4" class="fg-node-box" />
|
||||
@SvgText("10", "16", "fg-eyebrow", Eyebrow(node))
|
||||
@SvgText(F(NodeWidth - 10), "16", "fg-duration", Duration(node), anchor: "end")
|
||||
@SvgText("10", "40", "fg-title", Clip(node.Title, 20))
|
||||
@SvgText("10", "60", "fg-subtitle", Clip(node.Subtitle, 24))
|
||||
</g>
|
||||
}
|
||||
</svg>
|
||||
</div>
|
||||
Reference in New Issue
Block a user