Skip to content

Commit 08c4f0c

Browse files
committed
feat: directed graph component
This is super naive in placement and drawing of the splines. It hints that you can select things, but you can't do that yet either.
1 parent 71f9fea commit 08c4f0c

File tree

7 files changed

+616
-23
lines changed

7 files changed

+616
-23
lines changed

src/cli/dev/graph.rs

+89-21
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,112 @@
11
use cata::{Command, Container};
22
use clap::Parser;
33
use eyre::Result;
4-
use k8s_openapi::api::core::v1::Pod;
4+
use futures::stream::{StreamExt, TryStreamExt};
5+
use k8s_openapi::api::core::v1::{ObjectReference, Pod};
56
use kube::{api::ListParams, Api};
6-
use petgraph::dot::{Config, Dot};
7+
use petgraph::graph::Graph;
8+
use ratatui::{
9+
layout::Constraint,
10+
text::Text,
11+
widgets::{block::Title, Borders, Paragraph},
12+
Frame,
13+
};
14+
use tokio::io::AsyncReadExt;
715

8-
use crate::resources::ResourceGraph;
16+
use crate::{
17+
events::{Event, Keypress},
18+
resources::ResourceGraph,
19+
widget::graph,
20+
};
921

1022
#[derive(Parser, Container)]
1123
pub struct Cmd {}
1224

1325
#[async_trait::async_trait]
1426
impl Command for Cmd {
1527
async fn run(&self) -> Result<()> {
28+
let mut term = ratatui::init();
29+
1630
let client = kube::Client::try_default().await?;
1731

1832
let pods = Api::<Pod>::all(client.clone())
1933
.list(&ListParams::default())
2034
.await?;
2135

22-
for pod in pods.items {
23-
let g = pod.graph(&client).await?;
24-
25-
let ng = g.map(
26-
|_, n| {
27-
format!(
28-
"{}/{}",
29-
n.kind
30-
.as_ref()
31-
.unwrap_or(&"unknown".to_string())
32-
.to_lowercase(),
33-
n.name.as_ref().unwrap_or(&"unknown".to_string())
34-
)
35-
},
36-
|_, e| e,
37-
);
38-
39-
println!("{:#?}", Dot::with_config(&ng, &[Config::EdgeNoLabel]));
36+
let graphs = futures::stream::iter(pods.items)
37+
.then(|pod| {
38+
let client = client.clone();
39+
async move { pod.graph(&client).await }
40+
})
41+
.try_collect::<Vec<_>>()
42+
.await?;
43+
44+
let mut interval = tokio::time::interval(tokio::time::Duration::from_micros(100));
45+
let mut stdin = tokio::io::stdin();
46+
let mut buf = Vec::new();
47+
let mut i: usize = 0;
48+
49+
loop {
50+
tokio::select! {
51+
_ = stdin.read_buf(&mut buf) => {
52+
let ev = Event::from(buf.as_slice());
53+
buf.clear();
54+
55+
let Some(key) = ev.key() else {
56+
continue;
57+
};
58+
59+
tracing::info!("key: {:?}", key);
60+
61+
match key {
62+
Keypress::Escape => break,
63+
Keypress::CursorLeft => i = i.saturating_sub(1),
64+
Keypress::CursorRight => i = i.saturating_add(1),
65+
_ => {}
66+
}
67+
}
68+
_ = interval.tick() => {
69+
let g = &graphs.get(i % graphs.len()).unwrap();
70+
71+
term.draw(|frame| draw(frame, i, g))?;
72+
}
73+
}
4074
}
4175

4276
Ok(())
4377
}
4478
}
79+
80+
fn draw(frame: &mut Frame, i: usize, graph: &Graph<ObjectReference, ()>) {
81+
frame.render_widget(Paragraph::new(format!("{i}")), frame.area());
82+
83+
let ng = graph.map(
84+
|_, o| {
85+
graph::Node::builder()
86+
.text(Text::from(o.name.clone().unwrap_or("unknown".to_string())))
87+
.borders(Borders::ALL)
88+
.titles(vec![Title::default().content(
89+
o.kind
90+
.clone()
91+
.unwrap_or("unknown".to_string().to_lowercase()),
92+
)])
93+
.maybe_constraint(if o.kind == Some("Pod".to_string()) {
94+
Some(Constraint::Fill(0))
95+
} else {
96+
None
97+
})
98+
.build()
99+
},
100+
|_, ()| 0,
101+
);
102+
103+
let widget = graph::Directed::builder().graph(ng).build();
104+
105+
frame.render_stateful_widget_ref(widget, frame.area(), &mut graph::State::default());
106+
}
107+
108+
impl Drop for Cmd {
109+
fn drop(&mut self) {
110+
ratatui::restore();
111+
}
112+
}

src/widget.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod apex;
22
pub mod debug;
33
pub mod error;
4+
pub mod graph;
45
pub mod input;
56
pub mod loading;
67
pub mod log;

src/widget/graph.rs

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
mod line;
2+
mod node;
3+
mod placement;
4+
5+
use std::collections::BTreeMap;
6+
7+
use bon::Builder;
8+
use line::Line;
9+
pub use node::Node;
10+
use petgraph::graph::{Graph, NodeIndex};
11+
use ratatui::{
12+
buffer::Buffer,
13+
layout::{Position, Rect},
14+
widgets::{StatefulWidgetRef, Widget, WidgetRef},
15+
};
16+
17+
static PADDING: Rect = Rect {
18+
x: 0,
19+
y: 0,
20+
width: 2,
21+
height: 3,
22+
};
23+
24+
type NodeTree = BTreeMap<NodeIndex, Placement>;
25+
26+
#[derive(Debug, Builder)]
27+
struct Placement {
28+
idx: NodeIndex,
29+
rank: u16,
30+
#[builder(default)]
31+
pos: Rect,
32+
#[builder(default)]
33+
edges: Vec<Edge>,
34+
}
35+
36+
#[derive(Debug)]
37+
struct Edge {
38+
from: Position,
39+
to: Position,
40+
}
41+
42+
#[allow(dead_code)]
43+
#[derive(Default)]
44+
pub struct State {
45+
selected: Option<NodeIndex>,
46+
}
47+
48+
pub struct Directed<'a> {
49+
graph: Graph<node::Node<'a>, u16>,
50+
nodes: NodeTree,
51+
}
52+
53+
#[bon::bon]
54+
impl<'a> Directed<'a> {
55+
#[builder]
56+
pub fn new(graph: Graph<node::Node<'a>, u16>) -> Self {
57+
let mut nodes = placement::rank(&graph);
58+
placement::node(&graph, PADDING, &mut nodes);
59+
placement::edge(&graph, &mut nodes);
60+
61+
Self { graph, nodes }
62+
}
63+
}
64+
65+
impl Directed<'_> {
66+
fn node(&self, area: Rect, buffer: &mut Buffer, _: &mut State, node: &Placement) {
67+
let widget = &self.graph.raw_nodes()[node.idx.index()].weight;
68+
69+
let mut subview = node.pos;
70+
subview.x += area.x;
71+
subview.y += area.y;
72+
73+
if subview.x > area.width || subview.y > area.height {
74+
return;
75+
}
76+
77+
widget.render(subview, buffer);
78+
}
79+
}
80+
81+
impl StatefulWidgetRef for Directed<'_> {
82+
type State = State;
83+
84+
fn render_ref(&self, area: Rect, buffer: &mut Buffer, state: &mut Self::State) {
85+
self.nodes.iter().for_each(|(_, node)| {
86+
self.node(area, buffer, state, node);
87+
});
88+
89+
// The edges need to be drawn after nodes so that they can draw the connectors
90+
// correctly.
91+
self.nodes.iter().for_each(|(_, node)| {
92+
draw_edges(area, buffer, state, node);
93+
});
94+
}
95+
}
96+
97+
fn draw_edges(area: Rect, buffer: &mut Buffer, _: &mut State, node: &Placement) {
98+
for edge in &node.edges {
99+
Line::builder()
100+
.from(edge.from)
101+
.to(edge.to)
102+
.build()
103+
.render_ref(area, buffer);
104+
}
105+
}

0 commit comments

Comments
 (0)