Files
Ely-GPUI-Components/src/devtools/connections.rs
T
ZacharyZhang-NYandClaude Opus 5.5 4c9727d67c DB & Dev Tools · Databases: connections, schemas, queries, structure, Redis
A new chapter, src/devtools. ConnectionManager lists saved connections
with their engine and address, or why one failed, and connects on Enter;
ConnectionForm fills a connection with a port checked from 1 to 65535,
tests it and saves it. SchemaTree shows schemas, tables, views and
fields with keys marked; ERDiagram draws tables and their foreign keys
on NodeGraph, whose fixed wires show only the sockets that hold one.
QueryHistory finds past runs and runs one again; QueryPlanViewer shows
each step's share of the cost. TableStructureEditor edits fields in
place; IndexManager lists and draws up indexes; RedisKeyBrowser finds
keys by glob and lays out each type. QueryEditor is editor::CodeEditor,
whose lexer now knows SQL's words, and QueryResultGrid is
tables::DataGrid. lists::row_action keeps a row's button its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:51:45 -04:00

289 lines
9.2 KiB
Rust

use std::rc::Rc;
use gpui::{
App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
};
use crate::{
buttons::{Button, ButtonVariant, IconButton},
data_display::{Badge, Tone},
lists::{ListItem, SelectableList, row_action},
primitives::{Icon, IconName},
theme::{ActiveTheme, IconSize},
};
/// A database a connection reaches.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Engine {
Postgres,
MySql,
Sqlite,
Redis,
MongoDb,
}
impl Engine {
pub const ALL: [Engine; 5] = [
Engine::Postgres,
Engine::MySql,
Engine::Sqlite,
Engine::Redis,
Engine::MongoDb,
];
pub fn name(self) -> &'static str {
match self {
Engine::Postgres => "PostgreSQL",
Engine::MySql => "MySQL",
Engine::Sqlite => "SQLite",
Engine::Redis => "Redis",
Engine::MongoDb => "MongoDB",
}
}
/// The port it listens on unless told otherwise; SQLite reads a file.
pub fn port(self) -> Option<u16> {
match self {
Engine::Postgres => Some(5432),
Engine::MySql => Some(3306),
Engine::Sqlite => None,
Engine::Redis => Some(6379),
Engine::MongoDb => Some(27017),
}
}
pub(super) fn named(name: &str) -> Engine {
*Engine::ALL
.iter()
.find(|engine| engine.name() == name)
.unwrap_or_else(|| panic!("no engine {name}"))
}
}
/// Where a connection stands.
#[derive(Clone, Debug, PartialEq)]
pub enum ConnectionState {
Connected,
Idle,
Failed(SharedString),
}
/// A saved connection: its key and name, the engine, where it reaches, the database, who signs in, and where it stands. For SQLite the database is the file.
#[derive(Clone, Debug, PartialEq)]
pub struct Connection {
pub key: SharedString,
pub name: SharedString,
pub engine: Engine,
pub host: SharedString,
pub port: Option<u16>,
pub database: SharedString,
pub user: SharedString,
pub password: SharedString,
pub tls: bool,
pub state: ConnectionState,
}
impl Connection {
pub fn new(
key: impl Into<SharedString>,
name: impl Into<SharedString>,
engine: Engine,
) -> Self {
Self {
key: key.into(),
name: name.into(),
engine,
host: "localhost".into(),
port: engine.port(),
database: SharedString::default(),
user: SharedString::default(),
password: SharedString::default(),
tls: false,
state: ConnectionState::Idle,
}
}
/// Where it reaches, as one line: host and port and any database, or the file.
pub fn address(&self) -> String {
match (self.port, self.database.is_empty()) {
(Some(port), true) => format!("{}:{port}", self.host),
(Some(port), false) => format!("{}:{port}/{}", self.host, self.database),
(None, _) => self.database.to_string(),
}
}
}
type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
type Run = Rc<dyn Fn(&mut Window, &mut App)>;
/// Saved connections, each with its engine and address, or why it failed, and a badge for where it stands. Enter or a double press connects; the pencil edits and the bin removes; New asks for a blank one.
#[derive(IntoElement)]
pub struct ConnectionManager {
id: ElementId,
connections: Vec<Connection>,
selected: Option<SharedString>,
on_select: Option<OnKey>,
on_open: Option<OnKey>,
on_edit: Option<OnKey>,
on_remove: Option<OnKey>,
on_new: Option<Run>,
}
impl ConnectionManager {
pub fn new(
id: impl Into<ElementId>,
connections: impl IntoIterator<Item = Connection>,
) -> Self {
Self {
id: id.into(),
connections: connections.into_iter().collect(),
selected: None,
on_select: None,
on_open: None,
on_edit: None,
on_remove: None,
on_new: None,
}
}
pub fn selected(mut self, key: Option<impl Into<SharedString>>) -> Self {
self.selected = key.map(Into::into);
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
}
/// Gets the connection to connect, on Enter or a double press.
pub fn on_open(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_open = Some(Rc::new(handler));
self
}
pub fn on_edit(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_edit = Some(Rc::new(handler));
self
}
pub fn on_remove(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_remove = Some(Rc::new(handler));
self
}
pub fn on_new(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_new = Some(Rc::new(handler));
self
}
}
fn standing(state: &ConnectionState) -> (Tone, &'static str) {
match state {
ConnectionState::Connected => (Tone::Success, "Connected"),
ConnectionState::Idle => (Tone::Neutral, "Idle"),
ConnectionState::Failed(_) => (Tone::Danger, "Failed"),
}
}
impl RenderOnce for ConnectionManager {
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.connections.iter().fold(
SelectableList::new((id.clone(), "list")).selected(self.selected.clone()),
|list, connection| {
let key = connection.key.clone();
let (tone, words) = standing(&connection.state);
let detail = match &connection.state {
ConnectionState::Failed(why) => why.to_string(),
_ => format!("{} · {}", connection.engine.name(), connection.address()),
};
let action = |icon: IconName, tip: &'static str, handler: &Option<OnKey>| {
let (key, handler) = (key.clone(), handler.clone());
row_action(
IconButton::new((id.clone(), format!("{tip}-{key}")), icon)
.tooltip(tip)
.on_click(move |_, window, cx| {
if let Some(handler) = &handler {
handler(&key, window, cx);
}
}),
)
};
list.row(
key.clone(),
ListItem::new((id.clone(), format!("row-{key}")), connection.name.clone())
.description(detail)
.leading(
Icon::new(IconName::Database)
.size(IconSize::Md)
.color(muted),
)
.trailing(
div()
.flex()
.items_center()
.gap_1()
.child(Badge::new(words).tone(tone).dot())
.child(action(IconName::Pencil, "Edit", &self.on_edit))
.child(action(IconName::Trash2, "Remove", &self.on_remove)),
),
)
},
);
let (on_select, on_open, on_new) = (self.on_select, self.on_open, self.on_new);
let list = list
.on_change(move |keys, window, cx| {
if let (Some(key), Some(on_select)) = (keys.first(), &on_select) {
on_select(key, window, cx);
}
})
.on_activate(move |key, window, cx| {
log::info!("connection manager: open {key}");
if let Some(on_open) = &on_open {
on_open(key, window, cx);
}
});
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.flex()
.items_center()
.justify_between()
.gap_2()
.child(
div()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child("Connections"),
)
.child(
Button::new((id, "new"), "New")
.variant(ButtonVariant::Secondary)
.on_click(move |_, window, cx| {
if let Some(on_new) = &on_new {
on_new(window, cx);
}
}),
),
)
.child(list)
}
}