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>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-27 08:51:45 -04:00
1 parent 94ae7e73e7
commit 4c9727d67c
20 files changed
+2602 -14

No files matched your search

+223
View File
@@ -0,0 +1,223 @@
use std::rc::Rc;
use ely_gpui_component::{
devtools::{
Outcome, PlanStep, QueryHistory, QueryPlanViewer, QueryRun, RedisKey, RedisKeyBrowser,
RedisValue,
},
editor::CodeEditor,
forms::TextInput,
tables::DataGrid,
theme::{ActiveTheme, Radius},
};
use gpui::{
App, AppContext as _, IntoElement, ParentElement, SharedString, Styled, Window, div, px,
};
use jiff::Timestamp;
use crate::ui::{change, keep, section};
const SQL: &str = "SELECT u.email, count(o.id) AS orders, sum(o.total) AS spent
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.placed_at > now() - interval '30 days'
GROUP BY u.email
ORDER BY spent DESC
LIMIT 5;";
fn runs(now: Timestamp) -> Vec<QueryRun> {
let ago = |minutes: i64| now - jiff::SignedDuration::from_mins(minutes);
let run = |key: &str, sql: &str, minutes, took_ms, outcome| QueryRun {
key: key.to_string().into(),
sql: sql.to_string().into(),
at: ago(minutes),
took_ms,
outcome,
};
vec![
run(
"r4",
"SELECT u.email, count(o.id) AS orders",
2,
18,
Outcome::Rows(5),
),
run(
"r3",
"UPDATE orders SET total = total * 1.1 WHERE id = 42",
9,
4,
Outcome::Rows(1),
),
run(
"r2",
"SELECT * FROM order_item",
14,
2,
Outcome::Failed("relation \"order_item\" does not exist".into()),
),
run("r1", "SELECT count(*) FROM users", 61, 7, Outcome::Rows(1)),
]
}
fn plan() -> PlanStep {
PlanStep::new("limit", "Limit", 1284.0, 5, 12.4).child(
PlanStep::new("sort", "Sort", 1283.0, 212, 12.1).child(
PlanStep::new("group", "Group", 1270.0, 212, 11.2).child(
PlanStep::new("join", "Hash Join", 1150.0, 3480, 9.8)
.child(PlanStep::new("orders", "Seq Scan", 920.0, 3480, 7.6).on("orders"))
.child(PlanStep::new("users", "Index Scan", 180.0, 1200, 1.4).on("users_pkey")),
),
),
)
}
/// The query demo's editor, result, history search and clock.
struct Console {
editor: gpui::Entity<CodeEditor>,
rows: Rc<Vec<Vec<SharedString>>>,
search: gpui::Entity<TextInput>,
now: Timestamp,
runs: Vec<QueryRun>,
}
pub fn queries(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let console = window.use_keyed_state("devtools-console", cx, |window, cx| {
let now: Timestamp = "2026-09-27T12:00:00Z".parse().expect("a time");
let rows = [
["ada@example.com", "7", "1,284.00"],
["grace@example.com", "5", "962.50"],
["linus@example.com", "4", "711.20"],
["margaret@example.com", "4", "640.00"],
["alan@example.com", "3", "388.90"],
];
Console {
editor: cx.new(|cx| CodeEditor::new(SQL, window, cx).language("SQL")),
rows: Rc::new(
rows.iter()
.map(|row| row.iter().map(|cell| SharedString::from(*cell)).collect())
.collect(),
),
search: cx.new(|cx| TextInput::new(window, cx).placeholder("Find a query")),
now,
runs: runs(now),
}
});
let now = console.read(cx);
let (editor, rows, search, clock, history) = (
now.editor.clone(),
now.rows.clone(),
now.search.clone(),
now.now,
now.runs.clone(),
);
let again = console.clone();
let theme = cx.theme();
let boxed = |width: f32, height: f32| {
div()
.w(px(width))
.h(px(height))
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.overflow_hidden()
};
section(
"QueryEditor → editor::CodeEditor · QueryResultGrid → tables::DataGrid · QueryHistory · QueryPlanViewer",
"A query in the code editor over its result in the data grid; the queries that ran, found by their text, each with how it ended, and one run again on Enter; and the plan, each step with its share of the cost.",
cx,
)
.child(
div()
.flex()
.flex_col()
.gap_4()
.child(boxed(760.0, 170.0).child(editor))
.child(
DataGrid::new("devtools-result", ["email", "orders", "spent"], rows)
.w(px(760.))
.h(px(170.)),
)
.child(
div()
.flex()
.flex_wrap()
.gap_6()
.child(
div().w(px(340.)).child(
QueryHistory::new("devtools-history", history, &search, clock).on_run(move |key, _, cx| {
change(&again, cx, |console| {
let run = console.runs.iter().find(|run| run.key == *key).expect("a listed run").clone();
console.runs.insert(0, QueryRun { key: format!("r{}", console.runs.len() + 1).into(), at: console.now, ..run });
})
}),
),
)
.child(div().w(px(520.)).child(QueryPlanViewer::new("devtools-plan", plan()))),
),
)
}
pub fn redis(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let pattern = window.use_keyed_state("devtools-redis-pattern", cx, |window, cx| {
TextInput::new(window, cx).placeholder("Pattern, such as user:*")
});
let picked = keep(
"devtools-redis-picked",
|| Some(SharedString::from("user:42")),
window,
cx,
);
let selected = picked.read(cx).clone();
let keys = vec![
RedisKey {
key: "user:42".into(),
value: RedisValue::Hash(vec![
("name".into(), "Ada".into()),
("plan".into(), "team".into()),
("seats".into(), "12".into()),
]),
ttl: None,
},
RedisKey {
key: "user:42:session".into(),
value: RedisValue::Text("f3a9c1d2-77e0-4b1a".into()),
ttl: Some(1800),
},
RedisKey {
key: "queue:mail".into(),
value: RedisValue::List(vec![
"welcome:42".into(),
"receipt:1009".into(),
"digest:7".into(),
]),
ttl: None,
},
RedisKey {
key: "tags:popular".into(),
value: RedisValue::Set(vec!["rust".into(), "gpui".into(), "design".into()]),
ttl: None,
},
RedisKey {
key: "board:weekly".into(),
value: RedisValue::Sorted(vec![
("ada".into(), 1284.0),
("grace".into(), 962.5),
("alan".into(), 388.9),
]),
ttl: Some(86_400),
},
];
section(
"RedisKeyBrowser",
"Keys found by a glob, each with its type and the time it has left; the key picked shows what it holds, laid out for its type.",
cx,
)
.child(
div().w(px(760.)).child(
RedisKeyBrowser::new("devtools-redis", keys, &pattern)
.selected(selected)
.on_select(move |key, _, cx| change(&picked, cx, |picked| *picked = Some(key.clone()))),
),
)
}
+286
View File
@@ -0,0 +1,286 @@
use ely_gpui_component::{
canvas::Viewport,
devtools::{
Connection, ConnectionForm, ConnectionManager, ConnectionState, DbTable, ERDiagram, Engine,
Field, Index, IndexManager, Schema, SchemaTree, TableStructureEditor, TestState,
},
theme::{ActiveTheme, Radius},
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use crate::ui::{change, keep, section};
pub(super) fn tables() -> Vec<DbTable> {
vec![
DbTable::new(
"users",
[
Field::new("id", "int").primary(),
Field::new("email", "text"),
Field::new("name", "text").nullable(),
Field::new("created_at", "timestamptz").default_value("now()"),
],
),
DbTable::new(
"orders",
[
Field::new("id", "int").primary(),
Field::new("user_id", "int").refers("users", "id"),
Field::new("total", "numeric"),
Field::new("placed_at", "timestamptz"),
],
),
DbTable::new(
"order_items",
[
Field::new("order_id", "int").refers("orders", "id"),
Field::new("sku", "text"),
Field::new("quantity", "int").default_value("1"),
],
),
DbTable::new(
"recent_orders",
[Field::new("id", "int"), Field::new("total", "numeric")],
)
.view(),
]
}
/// The connections demo's list, the one picked, the one in the form, and its last test.
struct Desk {
connections: Vec<Connection>,
selected: Option<SharedString>,
editing: Connection,
test: Option<TestState>,
}
impl Desk {
fn new() -> Self {
let mut shop = Connection::new("shop", "Shop", Engine::Postgres);
shop.database = "shop".into();
shop.user = "app".into();
shop.state = ConnectionState::Connected;
let mut cache = Connection::new("cache", "Cache", Engine::Redis);
cache.host = "10.0.0.12".into();
let mut analytics = Connection::new("analytics", "Analytics", Engine::MySql);
analytics.host = "db.internal".into();
analytics.database = "events".into();
analytics.state = ConnectionState::Failed("Password rejected for user report".into());
let mut notes = Connection::new("notes", "Notes", Engine::Sqlite);
notes.database = "~/notes.db".into();
Self {
editing: shop.clone(),
connections: vec![shop, cache, analytics, notes],
selected: Some("shop".into()),
test: None,
}
}
}
pub fn connections(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let desk = keep("devtools-connections", Desk::new, window, cx);
let now = desk.read(cx);
let (connections, selected, editing, test) = (
now.connections.clone(),
now.selected.clone(),
now.editing.clone(),
now.test.clone(),
);
let [picked, opened, edited, removed, made, tested, saved] = [(); 7].map(|_| desk.clone());
let mut form = ConnectionForm::new("devtools-connection-form", editing)
.on_test(move |connection, _, cx| {
change(&tested, cx, |desk| {
desk.test = Some(match connection.engine {
Engine::MySql => TestState::Failed("Password rejected".into()),
_ => TestState::Passed(23),
})
})
})
.on_save(move |connection, _, cx| {
change(&saved, cx, |desk| {
match desk
.connections
.iter_mut()
.find(|each| each.key == connection.key)
{
Some(each) => *each = connection.clone(),
None => desk.connections.push(connection.clone()),
}
desk.selected = Some(connection.key.clone());
desk.test = None;
})
});
if let Some(test) = test {
form = form.test(test);
}
section(
"ConnectionManager · ConnectionForm",
"Saved connections with their engine and address, or why one failed. Enter or a double press connects; the pencil opens one in the form, where Test tries it and Save keeps it.",
cx,
)
.child(
div()
.flex()
.flex_wrap()
.gap_6()
.child(
div().w(px(340.)).child(
ConnectionManager::new("devtools-connections", connections)
.selected(selected)
.on_select(move |key, _, cx| change(&picked, cx, |desk| desk.selected = Some(key.clone())))
.on_open(move |key, _, cx| {
change(&opened, cx, |desk| {
let each = desk.connections.iter_mut().find(|each| each.key == *key).expect("a listed connection");
each.state = ConnectionState::Connected;
})
})
.on_edit(move |key, _, cx| {
change(&edited, cx, |desk| {
desk.editing = desk.connections.iter().find(|each| each.key == *key).expect("a listed connection").clone();
desk.test = None;
})
})
.on_remove(move |key, _, cx| change(&removed, cx, |desk| desk.connections.retain(|each| each.key != *key)))
.on_new(move |_, cx| {
change(&made, cx, |desk| {
let key = SharedString::from(format!("new-{}", desk.connections.len() + 1));
desk.editing = Connection::new(key, "", Engine::Postgres);
desk.test = None;
})
}),
),
)
.child(div().w(px(360.)).child(form)),
)
}
/// The schema demo's tree selection and where each table sits in the diagram.
struct Map {
selected: Vec<SharedString>,
places: Vec<(SharedString, (f32, f32))>,
view: Viewport,
}
pub fn schema(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let map = keep(
"devtools-schema",
|| Map {
selected: vec!["public.orders".into()],
places: vec![
("order_items".into(), (20.0, 40.0)),
("orders".into(), (300.0, 20.0)),
("users".into(), (580.0, 60.0)),
("recent_orders".into(), (20.0, 230.0)),
],
view: Viewport::new(0.0, 0.0, 0.7),
},
window,
cx,
);
let now = map.read(cx);
let (selected, places, view) = (now.selected.clone(), now.places.clone(), now.view);
let [picked, moved, viewed] = [(); 3].map(|_| map.clone());
let theme = cx.theme();
let boxed = |width: f32, height: f32| {
div()
.w(px(width))
.h(px(height))
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.overflow_hidden()
};
section(
"SchemaTree · ERDiagram",
"A database's schemas, tables and views, each field with its type and keys marked. The diagram draws each table as a box of fields and a line from every foreign key to the field it points at; a table's title drags it.",
cx,
)
.child(
div()
.flex()
.flex_wrap()
.gap_6()
.child(
boxed(260.0, 320.0).p_1().child(
SchemaTree::new(
"devtools-schema-tree",
[Schema { name: "public".into(), tables: tables() }],
)
.open(["public", "public.orders"])
.selected(selected)
.on_select(move |keys, _, cx| change(&picked, cx, |map| map.selected = keys.to_vec())),
),
)
.child(
boxed(560.0, 320.0).child(
ERDiagram::new("devtools-erd", view, tables(), places)
.on_move(move |name, (dx, dy), _, cx| {
change(&moved, cx, |map| {
let place = map.places.iter_mut().find(|(each, _)| each == name).expect("a placed table");
place.1 = (place.1.0 + dx, place.1.1 + dy);
})
})
.on_viewport(move |next, _, cx| change(&viewed, cx, |map| map.view = next)),
),
),
)
}
/// The structure demo's fields and indexes.
struct Build {
fields: Vec<Field>,
indexes: Vec<Index>,
}
pub fn structure(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let build = keep(
"devtools-structure",
|| Build {
fields: tables()[1].fields.clone(),
indexes: vec![
Index {
name: "orders_pkey".into(),
fields: vec!["id".into()],
unique: true,
method: "btree".into(),
},
Index {
name: "orders_user_placed".into(),
fields: vec!["user_id".into(), "placed_at".into()],
unique: false,
method: "btree".into(),
},
],
},
window,
cx,
);
let now = build.read(cx);
let (fields, indexes) = (now.fields.clone(), now.indexes.clone());
let names: Vec<SharedString> = fields.iter().map(|field| field.name.clone()).collect();
let [edited, dropped, created] = [(); 3].map(|_| build.clone());
section(
"TableStructureEditor · IndexManager",
"A table's fields edited in place: a name or a default rewritten on a press, a type, whether it may be empty, and the key. Its indexes list their fields and method, and a new one is drawn up from the table's fields.",
cx,
)
.child(
div()
.flex()
.flex_wrap()
.gap_6()
.child(
div().w(px(620.)).child(
TableStructureEditor::new("devtools-structure", fields)
.on_change(move |next, _, cx| change(&edited, cx, |build| build.fields = next)),
),
)
.child(
div().w(px(300.)).child(
IndexManager::new("devtools-indexes", indexes, names)
.on_drop(move |name, _, cx| change(&dropped, cx, |build| build.indexes.retain(|index| index.name != *name)))
.on_create(move |index, _, cx| change(&created, cx, |build| build.indexes.push(index))),
),
),
)
}
+28
View File
@@ -0,0 +1,28 @@
use gpui::{AnyElement, App, Window, div, prelude::*};
mod data;
mod db;
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 33,
slug: "devtools",
title: "DB & Dev Tools",
summary: "Connections, schemas and queries, data to read, requests to send, and the machines that run them.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(db::connections(window, cx))
.child(db::schema(window, cx))
.child(data::queries(window, cx))
.child(db::structure(window, cx))
.child(data::redis(window, cx))
.into_any_element()
}