Show teams in the workflow mermaid graph
This commit is contained in:
@@ -32,6 +32,7 @@ struct FlowNode {
|
||||
var shape: NodeShape = .box
|
||||
var classes: [String] = []
|
||||
var style = NodeColors()
|
||||
var members: [FlowNode] = []
|
||||
}
|
||||
|
||||
struct FlowEdge {
|
||||
@@ -269,8 +270,12 @@ struct PlacedNode {
|
||||
let center: CGPoint
|
||||
let size: CGSize
|
||||
let colors: NodeColors
|
||||
var members: [PlacedNode] = []
|
||||
}
|
||||
|
||||
private let groupPad: CGFloat = 10
|
||||
private let memberGap: CGFloat = 10
|
||||
|
||||
struct DiagramLayout {
|
||||
let nodes: [String: PlacedNode]
|
||||
let edges: [FlowEdge]
|
||||
@@ -385,21 +390,6 @@ func layoutFlowchart(_ chart: Flowchart) -> DiagramLayout {
|
||||
}
|
||||
}
|
||||
|
||||
var sizes: [String: CGSize] = [:]
|
||||
for n in ids {
|
||||
let node = chart.nodes[n]!
|
||||
let t = textSize(node.label, labelFont)
|
||||
switch node.shape {
|
||||
case .box, .rounded:
|
||||
sizes[n] = CGSize(width: t.width + 30, height: t.height + 18)
|
||||
case .diamond:
|
||||
sizes[n] = CGSize(width: (t.width * 0.75 + 14) * 2, height: (t.height * 1.5 + 8) * 2)
|
||||
case .circle:
|
||||
let d = (t.width * t.width + t.height * t.height).squareRoot() + 16
|
||||
sizes[n] = CGSize(width: d, height: d)
|
||||
}
|
||||
}
|
||||
|
||||
let horizontal = chart.direction == .lr || chart.direction == .rl
|
||||
let rankGap: CGFloat = horizontal ? 70 : 56
|
||||
let nodeGap: CGFloat = horizontal ? 24 : 32
|
||||
@@ -409,6 +399,47 @@ func layoutFlowchart(_ chart: Flowchart) -> DiagramLayout {
|
||||
func across(_ s: CGSize) -> CGFloat {
|
||||
horizontal ? s.height : s.width
|
||||
}
|
||||
|
||||
func nodeSize(_ node: FlowNode) -> CGSize {
|
||||
let t = textSize(node.label, labelFont)
|
||||
if !node.members.isEmpty {
|
||||
let ms = node.members.map(nodeSize)
|
||||
let band = groupPad + t.height + groupPad
|
||||
let gaps = memberGap * CGFloat(ms.count - 1)
|
||||
let inner = horizontal
|
||||
? CGSize(width: ms.map(\.width).max()!, height: ms.map(\.height).reduce(0, +) + gaps)
|
||||
: CGSize(width: ms.map(\.width).reduce(0, +) + gaps, height: ms.map(\.height).max()!)
|
||||
return CGSize(width: max(t.width + 30, inner.width + groupPad * 2), height: band + inner.height + groupPad)
|
||||
}
|
||||
switch node.shape {
|
||||
case .box, .rounded:
|
||||
return CGSize(width: t.width + 30, height: t.height + 18)
|
||||
case .diamond:
|
||||
return CGSize(width: (t.width * 0.75 + 14) * 2, height: (t.height * 1.5 + 8) * 2)
|
||||
case .circle:
|
||||
let d = (t.width * t.width + t.height * t.height).squareRoot() + 16
|
||||
return CGSize(width: d, height: d)
|
||||
}
|
||||
}
|
||||
func placeMembers(_ group: FlowNode, _ center: CGPoint, _ size: CGSize) -> [PlacedNode] {
|
||||
let ms = group.members.map(nodeSize)
|
||||
let top = center.y - size.height / 2 + groupPad + textSize(group.label, labelFont).height + groupPad
|
||||
let gaps = memberGap * CGFloat(ms.count - 1)
|
||||
var offset = horizontal ? top : center.x - (ms.map(\.width).reduce(0, +) + gaps) / 2
|
||||
let rowHeight = ms.map(\.height).max() ?? 0
|
||||
return zip(group.members, ms).map { m, s in
|
||||
let c = horizontal
|
||||
? CGPoint(x: center.x, y: offset + s.height / 2)
|
||||
: CGPoint(x: offset + s.width / 2, y: top + rowHeight / 2)
|
||||
offset += (horizontal ? s.height : s.width) + memberGap
|
||||
return PlacedNode(node: m, center: c, size: s, colors: m.style)
|
||||
}
|
||||
}
|
||||
|
||||
var sizes: [String: CGSize] = [:]
|
||||
for n in ids {
|
||||
sizes[n] = nodeSize(chart.nodes[n]!)
|
||||
}
|
||||
let spans = layers.map { l in l.map { across(sizes[$0]!) }.reduce(0, +) + nodeGap * CGFloat(max(0, l.count - 1)) }
|
||||
let widest = spans.max() ?? 0
|
||||
var centers: [String: CGPoint] = [:]
|
||||
@@ -437,7 +468,9 @@ func layoutFlowchart(_ chart: Flowchart) -> DiagramLayout {
|
||||
if chart.direction == .bt {
|
||||
c.y = total.height - c.y
|
||||
}
|
||||
placed[n] = PlacedNode(node: chart.nodes[n]!, center: CGPoint(x: c.x + margin, y: c.y + margin), size: sizes[n]!, colors: chart.colors(n))
|
||||
let center = CGPoint(x: c.x + margin, y: c.y + margin)
|
||||
let node = chart.nodes[n]!
|
||||
placed[n] = PlacedNode(node: node, center: center, size: sizes[n]!, colors: chart.colors(n), members: placeMembers(node, center, sizes[n]!))
|
||||
}
|
||||
return DiagramLayout(nodes: placed, edges: chart.edges, size: CGSize(width: total.width + margin * 2, height: total.height + margin * 2))
|
||||
}
|
||||
@@ -532,18 +565,26 @@ struct FlowchartCanvas: View {
|
||||
}
|
||||
}
|
||||
for (_, n) in layout.nodes {
|
||||
let path = shapePath(n)
|
||||
context.fill(path, with: .color(n.colors.fill ?? Palette.surface))
|
||||
context.stroke(path, with: .color(n.colors.stroke ?? Palette.border), lineWidth: 1)
|
||||
for (i, line) in n.node.label.components(separatedBy: "\n").enumerated() {
|
||||
let count = CGFloat(n.node.label.components(separatedBy: "\n").count)
|
||||
let lineHeight = textSize("X", labelFont).height
|
||||
let y = n.center.y + (CGFloat(i) - (count - 1) / 2) * lineHeight
|
||||
context.draw(Text(line).font(Font(labelFont)).foregroundColor(n.colors.text ?? Palette.text), at: CGPoint(x: n.center.x, y: y), anchor: .center)
|
||||
drawNode(&context, n)
|
||||
for m in n.members {
|
||||
drawNode(&context, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func drawNode(_ context: inout GraphicsContext, _ n: PlacedNode) {
|
||||
let path = shapePath(n)
|
||||
context.fill(path, with: .color(n.colors.fill ?? Palette.surface))
|
||||
context.stroke(path, with: .color(n.colors.stroke ?? Palette.border), lineWidth: 1)
|
||||
let lines = n.node.label.components(separatedBy: "\n")
|
||||
let lineHeight = textSize("X", labelFont).height
|
||||
let middle = n.members.isEmpty ? n.center.y : n.center.y - n.size.height / 2 + groupPad + lineHeight * CGFloat(lines.count) / 2
|
||||
for (i, line) in lines.enumerated() {
|
||||
let y = middle + (CGFloat(i) - CGFloat(lines.count - 1) / 2) * lineHeight
|
||||
context.draw(Text(line).font(Font(labelFont)).foregroundColor(n.colors.text ?? Palette.text), at: CGPoint(x: n.center.x, y: y), anchor: .center)
|
||||
}
|
||||
}
|
||||
|
||||
private func arrowHead(_ context: inout GraphicsContext, from: CGPoint, to: CGPoint) {
|
||||
let angle = atan2(to.y - from.y, to.x - from.x)
|
||||
let size: CGFloat = 9
|
||||
|
||||
@@ -15,12 +15,19 @@ private let stateColors: [RunStage.StatePayload: NodeColors] = [
|
||||
.discarded: NodeColors(fill: Palette.dangerTint, stroke: Palette.danger, text: Palette.text),
|
||||
]
|
||||
|
||||
private func stageSteps(_ steps: [WorkflowStep]) -> [WorkflowStep] {
|
||||
steps.flatMap { s in s.order.map(stageSteps) ?? s.parallel.map(stageSteps) ?? [s] }
|
||||
}
|
||||
|
||||
private func stageGraph(_ run: WorkflowRun) -> Flowchart {
|
||||
var chart = Flowchart(direction: .td)
|
||||
let steps = Dictionary(stageSteps(run.definition.steps).map { ($0.stage ?? "", $0) }, uniquingKeysWith: { a, _ in a })
|
||||
for (i, s) in run.stages.enumerated() {
|
||||
let id = "s\(i)"
|
||||
let colors = stateColors[s.state]!
|
||||
chart.node(id, label: s.name, shape: .box)
|
||||
chart.nodes[id]!.style = stateColors[s.state]!
|
||||
chart.nodes[id]!.style = colors
|
||||
chart.nodes[id]!.members = teamNodes(id, steps[s.name]?.agents, stroke: colors.stroke)
|
||||
}
|
||||
let ids = Dictionary(run.stages.enumerated().map { ($0.element.name, "s\($0.offset)") }, uniquingKeysWith: { a, _ in a })
|
||||
for s in run.stages {
|
||||
|
||||
@@ -92,6 +92,12 @@ private func newStage(_ root: StepNode, _ model: String) -> StepNode {
|
||||
return s
|
||||
}
|
||||
|
||||
func teamNodes(_ id: String, _ agents: [TeamMember]?, stroke: Color?) -> [FlowNode] {
|
||||
(agents ?? []).enumerated().map { i, a in
|
||||
FlowNode(id: "\(id)m\(i)", label: "\(a.name.isEmpty ? " " : a.name)\n\(a.model)", style: NodeColors(fill: Palette.bg, stroke: stroke, text: Palette.text))
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func stepGraph(_ root: StepNode) -> Flowchart {
|
||||
var chart = Flowchart(direction: .td)
|
||||
@@ -104,6 +110,7 @@ private func stepGraph(_ root: StepNode) -> Flowchart {
|
||||
case .stage:
|
||||
let id = "n\(chart.order.count)"
|
||||
chart.node(id, label: node.name.isEmpty ? " " : node.name, shape: .box)
|
||||
chart.nodes[id]!.members = teamNodes(id, node.agents, stroke: nil)
|
||||
for b in before {
|
||||
chart.edges.append(FlowEdge(from: b, to: id, label: nil, style: .solid, arrow: true))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user