artifactize_core/monitor/model/
graph.rs1use std::collections::BTreeMap;
4
5use super::{
6 NodeId, Target,
7 states::{Activity, Busy, Completion, EvalView, NotRun},
8 tree::{Kind, Node},
9};
10use crate::types::ArtifactName;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum Light {
17 Red,
19 Error,
21 Active,
23 Blocked,
25 Waiting,
28 Green,
30}
31
32impl From<Completion> for Light {
33 fn from(completion: Completion) -> Self {
35 match completion {
36 Completion::Complete => Self::Green,
37 Completion::Failed { verdict: true } => Self::Red,
38 Completion::Failed { verdict: false } => Self::Error,
39 Completion::InProgress(Busy::Running | Busy::Human) => Self::Active,
40 Completion::InProgress(Busy::Queued) => Self::Waiting,
41 Completion::Waiting { blocked: true } => Self::Blocked,
42 Completion::Waiting { blocked: false } => Self::Waiting,
43 }
44 }
45}
46
47impl From<&EvalView> for Light {
48 fn from(view: &EvalView) -> Self {
49 match view {
50 EvalView::Done(_) => Self::Green,
51 EvalView::Failed { verdict: true } => Self::Red,
52 EvalView::Failed { verdict: false } => Self::Error,
53 EvalView::InProgress(Activity::Running | Activity::Human(_)) => Self::Active,
54 EvalView::BlockedBy(_) => Self::Blocked,
55 EvalView::InProgress(Activity::Queued(_))
56 | EvalView::WaitingOn(_)
57 | EvalView::NotRun(
58 NotRun::Dependency(_)
59 | NotRun::Budget
60 | NotRun::Stale
61 | NotRun::Unreviewed
62 | NotRun::Absent,
63 ) => Self::Waiting,
64 }
65 }
66}
67
68impl Light {
69 fn holds(self) -> bool {
71 matches!(self, Self::Red | Self::Error | Self::Blocked)
72 }
73}
74
75fn eval_light(eval: &Node, view: &EvalView, lights: &BTreeMap<ArtifactName, Light>) -> Light {
79 let light = Light::from(view);
80 let waits = matches!(
81 view,
82 EvalView::WaitingOn(_) | EvalView::NotRun(NotRun::Dependency(_))
83 );
84 let held = eval.upstream.iter().any(|upstream| {
85 lights
86 .get(&upstream.artifact)
87 .copied()
88 .unwrap_or(upstream.completion.into())
89 .holds()
90 });
91 if waits && held { Light::Blocked } else { light }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct GraphNode {
96 pub id: NodeId,
98 pub name: ArtifactName,
99 pub light: Light,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct GraphEdge {
105 pub from: ArtifactName,
106 pub to: ArtifactName,
107 pub light: Light,
110 pub dependency: bool,
112}
113
114#[derive(Debug, Clone, Default, PartialEq, Eq)]
115pub struct Graph {
116 pub nodes: Vec<GraphNode>,
118 pub edges: Vec<GraphEdge>,
120}
121
122pub fn graph(tree: &[Node]) -> Graph {
125 let artifacts: Vec<(ArtifactName, &Node, Completion)> = tree
126 .iter()
127 .filter_map(|node| match node.kind {
128 Kind::Artifact { completion, .. } => match Target::from(&node.id) {
129 Target::Artifact(name) => Some((name, node, completion)),
130 _ => None,
131 },
132 Kind::Eval(_) => None,
133 })
134 .collect();
135 let mut lights: BTreeMap<ArtifactName, Light> = BTreeMap::new();
136 for (name, node, completion) in &artifacts {
137 let evals = node
139 .children
140 .iter()
141 .filter_map(|eval| match &eval.kind {
142 Kind::Eval(view) => Some(eval_light(eval, view, &lights)),
143 Kind::Artifact { .. } => None,
144 })
145 .min()
146 .unwrap_or(Light::Green);
147 let light = match (evals, completion) {
149 (Light::Green, Completion::Waiting { .. }) => Light::from(*completion),
150 (light, _) => light,
151 };
152 lights.insert(name.clone(), light);
153 }
154 let position: BTreeMap<&ArtifactName, usize> = artifacts
155 .iter()
156 .enumerate()
157 .map(|(index, (name, _, _))| (name, index))
158 .collect();
159 let mut edges = Vec::new();
160 for (name, node, _) in &artifacts {
161 let mut upstream: BTreeMap<&ArtifactName, Option<Light>> = BTreeMap::new();
163 for eval in &node.children {
164 let Kind::Eval(view) = &eval.kind else {
165 continue;
166 };
167 for dependency in &eval.upstream {
168 let lit = upstream.entry(&dependency.artifact).or_default();
169 if eval.dependency {
170 let light = eval_light(eval, view, &lights);
171 *lit = Some(lit.map_or(light, |lit| lit.min(light)));
172 }
173 }
174 }
175 for (from, lit) in upstream {
176 let Some(&own) = lights.get(from).filter(|_| from != name) else {
177 continue;
178 };
179 edges.push(GraphEdge {
180 from: from.clone(),
181 to: name.clone(),
182 light: lit.unwrap_or(own),
183 dependency: lit.is_some(),
184 });
185 }
186 }
187 edges.sort_by_key(|edge| (position[&edge.to], position[&edge.from]));
188 Graph {
189 nodes: artifacts
190 .into_iter()
191 .map(|(name, node, _)| GraphNode {
192 id: node.id.clone(),
193 light: lights[&name],
194 name,
195 })
196 .collect(),
197 edges,
198 }
199}