Files
Ely-GPUI-Components/src/devtools/machines.rs
T
ZacharyZhang-NY f7013b17ef DB & Dev Tools · Tools and machines: regex, encoders, contrast, containers, pods
RegexTester tries a pattern with its flags against a sample, washes
each match and lists it with its groups, zero-width ones too; a pattern
that does not compile says why. Encoder turns text into Base64, URL or
hex and back, says why a decode fails (Base64 pads end the text, twice
at most) and copies the result, drawn through typography::literal so no
ligature hides a character. ColorContrastChecker measures text on a
background, translucent text as it blends, and names the WCAG levels
met. ContainerList starts, stops, restarts and opens containers, each
button shown when the owner handles it. PodList sorts pods by any
column, ages by length through tables' new Cell::Duration, and narrows
them by namespace. ResourceMonitor shows each reading's latest value
beside its sparkline and, with a limit, how full it is.
2026-09-27 11:02:35 -04:00

391 lines
13 KiB
Rust

use std::rc::Rc;
use gpui::{
App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
};
use jiff::Timestamp;
use crate::{
buttons::IconButton,
data_display::{Badge, Tone},
forms::{Choice, Select},
lists::{ListItem, SelectableList, row_action},
primitives::{Icon, IconName},
tables::{Cell, Column, DataTable, Row},
theme::{ActiveTheme, IconSize},
typography::format,
};
/// Where a container stands.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ContainerState {
Running,
Paused,
Restarting,
/// Stopped, with the code it ended with.
Exited(i32),
}
impl ContainerState {
fn badge(self) -> (Tone, String) {
match self {
ContainerState::Running => (Tone::Success, "Running".into()),
ContainerState::Paused => (Tone::Warning, "Paused".into()),
ContainerState::Restarting => (Tone::Info, "Restarting".into()),
ContainerState::Exited(0) => (Tone::Neutral, "Exited".into()),
ContainerState::Exited(code) => (Tone::Danger, format!("Exited {code}")),
}
}
}
/// A container: its key and name, its image, where it stands, its published ports, its share of a CPU and its memory in bytes.
#[derive(Clone, Debug, PartialEq)]
pub struct Container {
pub key: SharedString,
pub name: SharedString,
pub image: SharedString,
pub state: ContainerState,
pub ports: Vec<SharedString>,
pub cpu: f32,
pub memory: u64,
}
type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
/// Containers on a host, each with its image and ports, its load while it runs, and where it stands. Its buttons, each shown when the owner handles it, start or stop it, restart it and open its logs; Enter or a double press opens it.
#[derive(IntoElement)]
pub struct ContainerList {
id: ElementId,
containers: Vec<Container>,
on_open: Option<OnKey>,
on_toggle: Option<OnKey>,
on_restart: Option<OnKey>,
on_logs: Option<OnKey>,
}
impl ContainerList {
pub fn new(id: impl Into<ElementId>, containers: impl IntoIterator<Item = Container>) -> Self {
Self {
id: id.into(),
containers: containers.into_iter().collect(),
on_open: None,
on_toggle: None,
on_restart: None,
on_logs: None,
}
}
pub fn on_open(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_open = Some(Rc::new(handler));
self
}
/// Asked to stop a running container, or to start any other.
pub fn on_toggle(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_toggle = Some(Rc::new(handler));
self
}
pub fn on_restart(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_restart = Some(Rc::new(handler));
self
}
pub fn on_logs(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_logs = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ContainerList {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let theme = cx.theme();
let muted = theme.colors.fg_muted;
let list = self.containers.iter().fold(
SelectableList::new((id.clone(), "list")),
|list, container| {
let key = container.key.clone();
let running = container.state == ContainerState::Running;
let (tone, words) = container.state.badge();
let button = |icon: IconName, tip: &'static str, handler: &Option<OnKey>| {
handler.clone().map(|handler| {
let key = key.clone();
row_action(
IconButton::new((id.clone(), format!("{tip}-{key}")), icon)
.tooltip(tip)
.on_click(move |_, window, cx| {
log::info!("container list: {tip} {key}");
handler(&key, window, cx);
}),
)
})
};
let load = if running {
format!(
" · {} · {}",
format::percent(container.cpu as f64, 0, false),
format::file_size(container.memory, true)
)
} else {
String::new()
};
let ports = if container.ports.is_empty() {
String::new()
} else {
format!(" · {}", container.ports.join(", "))
};
list.row(
key.clone(),
ListItem::new((id.clone(), format!("row-{key}")), container.name.clone())
.description(format!("{}{ports}{load}", container.image))
.leading(Icon::new(IconName::Package).size(IconSize::Md).color(muted))
.trailing(
div()
.flex()
.items_center()
.gap_1()
.child(Badge::new(words).tone(tone).dot())
.children(button(
if running {
IconName::CircleStop
} else {
IconName::Play
},
if running { "Stop" } else { "Start" },
&self.on_toggle,
))
.children(button(IconName::RotateCw, "Restart", &self.on_restart))
.children(button(IconName::Terminal, "Logs", &self.on_logs)),
),
)
},
);
match self.on_open {
Some(on_open) => list.on_activate(move |key, window, cx| {
log::info!("container list: open {key}");
on_open(key, window, cx)
}),
None => list,
}
}
}
/// A pod's phase, as Kubernetes names it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PodPhase {
Pending,
Running,
Succeeded,
Failed,
Unknown,
}
impl PodPhase {
fn cell(self) -> Cell {
let (words, tone) = match self {
PodPhase::Pending => ("Pending", Tone::Warning),
PodPhase::Running => ("Running", Tone::Success),
PodPhase::Succeeded => ("Succeeded", Tone::Neutral),
PodPhase::Failed => ("Failed", Tone::Danger),
PodPhase::Unknown => ("Unknown", Tone::Neutral),
};
Cell::Tag(words.into(), tone)
}
}
/// A pod: its key and name, its namespace, its phase, its containers ready of all, its restarts, when it started, and its node.
#[derive(Clone, Debug, PartialEq)]
pub struct Pod {
pub key: SharedString,
pub name: SharedString,
pub namespace: SharedString,
pub phase: PodPhase,
pub ready: (u32, u32),
pub restarts: u32,
pub started: Timestamp,
pub node: SharedString,
}
type OnKeys = Rc<dyn Fn(&[SharedString], &mut Window, &mut App)>;
/// Pods in a table, sortable by any column: name, namespace, phase, containers ready, restarts, age and node; a namespace narrows them.
#[derive(IntoElement)]
pub struct PodList {
id: ElementId,
pods: Vec<Pod>,
now: Timestamp,
selected: Vec<SharedString>,
on_select: Option<OnKeys>,
}
impl PodList {
/// `now` ages each pod.
pub fn new(
id: impl Into<ElementId>,
pods: impl IntoIterator<Item = Pod>,
now: Timestamp,
) -> Self {
Self {
id: id.into(),
pods: pods.into_iter().collect(),
now,
selected: Vec::new(),
on_select: None,
}
}
pub fn selected(mut self, keys: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
self.selected = keys.into_iter().map(Into::into).collect();
self
}
pub fn on_select(
mut self,
handler: impl Fn(&[SharedString], &mut Window, &mut App) + 'static,
) -> Self {
self.on_select = Some(Rc::new(handler));
self
}
}
/// Seconds from `started` to `now`; a start ahead of now, as clocks between machines drift, reads as now.
pub(crate) fn running_for(started: Timestamp, now: Timestamp) -> u64 {
(now.as_second() - started.as_second()).max(0) as u64
}
/// The namespaces among `pods`, in the order they first come.
fn namespaces(pods: &[Pod]) -> Vec<SharedString> {
let mut seen: Vec<SharedString> = Vec::new();
for pod in pods {
if !seen.contains(&pod.namespace) {
seen.push(pod.namespace.clone());
}
}
seen
}
/// The pods in `namespace`; the empty namespace, which Kubernetes never names, stands for all.
fn in_namespace<'a>(pods: &'a [Pod], namespace: &'a str) -> impl Iterator<Item = &'a Pod> {
pods.iter()
.filter(move |pod| namespace.is_empty() || pod.namespace == namespace)
}
impl RenderOnce for PodList {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let space = window.use_keyed_state((id.clone(), "namespace"), cx, |_, _| {
SharedString::default()
});
let chosen = space.read(cx).clone();
let spaces = namespaces(&self.pods);
let rows: Vec<Row> = in_namespace(&self.pods, &chosen)
.map(|pod| {
Row::new(
pod.key.clone(),
[
Cell::Text(pod.name.clone()),
Cell::Text(pod.namespace.clone()),
pod.phase.cell(),
Cell::Text(format!("{}/{}", pod.ready.0, pod.ready.1).into()),
Cell::Number(pod.restarts as f64),
Cell::Duration(running_for(pod.started, self.now)),
Cell::Text(pod.node.clone()),
],
)
})
.collect();
let theme = cx.theme();
let mut table = DataTable::new(
(id.clone(), "table"),
[
Column::new("name", "Name"),
Column::new("namespace", "Namespace"),
Column::new("phase", "Status"),
Column::new("ready", "Ready"),
Column::new("restarts", "Restarts").end(),
Column::new("age", "Age"),
Column::new("node", "Node"),
],
)
.rows(rows)
.selected(self.selected);
if let Some(on_select) = self.on_select {
table = table.on_select(move |keys, window, cx| on_select(keys, window, cx));
}
div()
.flex()
.flex_col()
.gap_2()
.child(
div().max_w(gpui::rems(theme.label_width().0 * 2.0)).child(
Select::new(
(id, "spaces"),
[Choice::new("", "All namespaces")].into_iter().chain(
spaces
.iter()
.map(|space| Choice::new(space.clone(), space.clone())),
),
)
.selected(chosen)
.on_change(move |value, _, cx| {
space.update(cx, |space, cx| {
*space = value.clone();
cx.notify();
})
}),
),
)
.child(table)
}
}
#[cfg(test)]
mod tests {
use super::{Pod, PodPhase, in_namespace, namespaces, running_for};
#[test]
fn a_pod_runs_from_its_start_and_a_clock_ahead_reads_as_now() {
let now: jiff::Timestamp = "2026-09-27T12:00:00Z".parse().expect("a time");
let started: jiff::Timestamp = "2026-09-27T10:35:00Z".parse().expect("a time");
assert_eq!(running_for(started, now), 5_100);
assert_eq!(running_for(now, started), 0);
}
#[test]
fn namespaces_come_once_in_order_and_narrow_the_pods() {
let now: jiff::Timestamp = "2026-09-27T12:00:00Z".parse().expect("a time");
let pod = |key: &str, namespace: &str| Pod {
key: key.to_string().into(),
name: key.to_string().into(),
namespace: namespace.to_string().into(),
phase: PodPhase::Running,
ready: (1, 1),
restarts: 0,
started: now,
node: "n1".into(),
};
let pods = [pod("a", "web"), pod("b", "jobs"), pod("c", "web")];
assert_eq!(namespaces(&pods), ["web", "jobs"]);
let keys = |namespace| {
in_namespace(&pods, namespace)
.map(|pod| pod.key.to_string())
.collect::<Vec<_>>()
};
assert_eq!(keys("web"), ["a", "c"]);
assert_eq!(keys(""), ["a", "b", "c"]);
}
}