1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
import { useNavigate } from "react-router";
import type { ChainStep, ContractPhase } from "../../lib/api";
import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar";
interface StepDiagramProps {
steps: ChainStep[];
}
const statusBorderColors: Record<string, string> = {
pending: "border-[#555]",
running: "border-yellow-400",
passed: "border-green-400",
failed: "border-red-400",
};
const statusDotColors: Record<string, string> = {
pending: "bg-[#555]",
running: "bg-yellow-400",
passed: "bg-green-400",
failed: "bg-red-400",
};
/**
* Assign depth to each step via topological sort.
* Steps with no dependsOn = depth 0. Steps depending only on depth-0 = depth 1. Etc.
*/
function assignDepths(steps: ChainStep[]): Map<string, number> {
const depths = new Map<string, number>();
const stepMap = new Map(steps.map((s) => [s.id, s]));
function getDepth(id: string): number {
if (depths.has(id)) return depths.get(id)!;
const step = stepMap.get(id);
if (!step || !step.dependsOn || step.dependsOn.length === 0) {
depths.set(id, 0);
return 0;
}
const maxParent = Math.max(
...step.dependsOn.map((depId) => getDepth(depId))
);
const d = maxParent + 1;
depths.set(id, d);
return d;
}
for (const step of steps) {
getDepth(step.id);
}
return depths;
}
export function StepDiagram({ steps }: StepDiagramProps) {
const navigate = useNavigate();
if (steps.length === 0) {
return (
<p className="font-mono text-xs text-[#7788aa]">No steps to display.</p>
);
}
const depths = assignDepths(steps);
const maxDepth = Math.max(...Array.from(depths.values()));
// Group steps by depth
const levels: ChainStep[][] = [];
for (let d = 0; d <= maxDepth; d++) {
levels.push(
steps
.filter((s) => depths.get(s.id) === d)
.sort((a, b) => a.orderIndex - b.orderIndex)
);
}
// Build position map for connectors
const stepPositions = new Map<string, { level: number; index: number }>();
levels.forEach((level, li) => {
level.forEach((step, si) => {
stepPositions.set(step.id, { level: li, index: si });
});
});
return (
<div className="space-y-3">
{levels.map((level, li) => (
<div key={li} className="flex items-start gap-2 flex-wrap">
{li > 0 && (
<div className="w-full flex justify-center mb-1">
<div className="w-px h-3 bg-[rgba(117,170,252,0.3)]" />
</div>
)}
{level.map((step) => {
const borderColor =
statusBorderColors[step.status] || "border-[#555]";
const dotColor = statusDotColors[step.status] || "bg-[#555]";
const summary = step.contractSummary;
const hasContract = !!step.contractId;
return (
<div
key={step.id}
className={`
border ${borderColor} bg-[rgba(0,0,0,0.2)] p-2 min-w-[180px] max-w-[220px]
${hasContract ? "cursor-pointer hover:bg-[rgba(117,170,252,0.05)]" : ""}
transition-colors
`}
onClick={() => {
if (hasContract) navigate(`/contracts/${step.contractId}`);
}}
title={hasContract ? "View contract" : undefined}
>
<div className="flex items-center gap-1.5 mb-1">
<div className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
<span className="font-mono text-[11px] text-[#dbe7ff] truncate flex-1">
{step.name}
</span>
{hasContract && (
<span className="font-mono text-[9px] text-[#75aafc] shrink-0">
→
</span>
)}
</div>
{summary && (
<>
<div className="mb-1">
<PhaseProgressBarCompact
currentPhase={summary.phase as ContractPhase}
/>
</div>
<div className="font-mono text-[9px] text-[#7788aa]">
{summary.tasksDone}/{summary.taskCount} tasks
{summary.tasksRunning > 0 && (
<span className="text-yellow-400 ml-1">
{summary.tasksRunning} running
</span>
)}
{summary.tasksFailed > 0 && (
<span className="text-red-400 ml-1">
{summary.tasksFailed} failed
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
))}
</div>
);
}
|