feat: name saved connection configurations
This commit is contained in:
+77
-14
@@ -133,7 +133,7 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
})
|
})
|
||||||
.map(|connected| {
|
.map(|connected| {
|
||||||
browser = Some(connected);
|
browser = Some(connected);
|
||||||
connection_label = Some(form.label());
|
connection_label = Some(form.connection_label());
|
||||||
active =
|
active =
|
||||||
selection.selected().and_then(|index| {
|
selection.selected().and_then(|index| {
|
||||||
match entries[index] {
|
match entries[index] {
|
||||||
@@ -147,6 +147,8 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
profiles.save(&form).map(|()| {
|
profiles.save(&form).map(|()| {
|
||||||
|
let saved_index = profiles.entries.iter().position(|entry| entry.id == form.id)
|
||||||
|
.unwrap_or(profiles.entries.len() - 1);
|
||||||
let updated = explorer_entries(
|
let updated = explorer_entries(
|
||||||
profiles.entries.len(),
|
profiles.entries.len(),
|
||||||
active,
|
active,
|
||||||
@@ -156,7 +158,7 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
selection.select(updated.iter().position(|entry| {
|
selection.select(updated.iter().position(|entry| {
|
||||||
*entry
|
*entry
|
||||||
== ExplorerEntry::Database(Some(
|
== ExplorerEntry::Database(Some(
|
||||||
profiles.entries.len() - 1,
|
saved_index,
|
||||||
))
|
))
|
||||||
}));
|
}));
|
||||||
focus = Focus::Explorer;
|
focus = Focus::Explorer;
|
||||||
@@ -205,6 +207,18 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
form = Form::default();
|
form = Form::default();
|
||||||
show_form = true;
|
show_form = true;
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('e') if focus == Focus::Explorer => {
|
||||||
|
let index = selection.selected().and_then(|index| match entries.get(index) {
|
||||||
|
Some(ExplorerEntry::Database(Some(index))) => Some(*index),
|
||||||
|
Some(ExplorerEntry::Relation(_)) => active,
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
if let Some(index) = index {
|
||||||
|
form = profiles.entries[index].clone();
|
||||||
|
form.focus = form.name_focus();
|
||||||
|
show_form = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
KeyCode::Char('i') if focus == Focus::Explorer => {
|
KeyCode::Char('i') if focus == Focus::Explorer => {
|
||||||
info =
|
info =
|
||||||
selection
|
selection
|
||||||
@@ -243,7 +257,7 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
match form.connect().and_then(App::new) {
|
match form.connect().and_then(App::new) {
|
||||||
Ok(connected) => {
|
Ok(connected) => {
|
||||||
browser = Some(connected);
|
browser = Some(connected);
|
||||||
connection_label = Some(form.label());
|
connection_label = Some(form.connection_label());
|
||||||
active = Some(index);
|
active = Some(index);
|
||||||
selection.select(Some(index));
|
selection.select(Some(index));
|
||||||
expanded = true;
|
expanded = true;
|
||||||
@@ -542,10 +556,14 @@ impl Profiles {
|
|||||||
path TEXT NOT NULL
|
path TEXT NOT NULL
|
||||||
)",
|
)",
|
||||||
)?;
|
)?;
|
||||||
let entries = storage.prepare("SELECT provider, host, port, database_name, username, tls, path, id FROM connections ORDER BY id")?
|
if !storage.prepare("SELECT name FROM pragma_table_info('connections') WHERE name = 'name'")?.exists([])? {
|
||||||
|
storage.execute_batch("ALTER TABLE connections ADD COLUMN name TEXT NOT NULL DEFAULT ''")?;
|
||||||
|
}
|
||||||
|
let entries = storage.prepare("SELECT provider, host, port, database_name, username, tls, path, id, name FROM connections ORDER BY id")?
|
||||||
.query_map([], |row| {
|
.query_map([], |row| {
|
||||||
let mut form = Form {
|
let mut form = Form {
|
||||||
id: Some(row.get(7)?),
|
id: Some(row.get(7)?),
|
||||||
|
name: Input::from(row.get::<_, String>(8)?.as_str()),
|
||||||
provider: usize::from(row.get::<_, u8>(0)?),
|
provider: usize::from(row.get::<_, u8>(0)?),
|
||||||
tls: usize::from(row.get::<_, u8>(5)?),
|
tls: usize::from(row.get::<_, u8>(5)?),
|
||||||
path: Input::from(row.get::<_, String>(6)?.as_str()),
|
path: Input::from(row.get::<_, String>(6)?.as_str()),
|
||||||
@@ -565,6 +583,7 @@ impl Profiles {
|
|||||||
saved.clear_password();
|
saved.clear_password();
|
||||||
saved.reconnect = false;
|
saved.reconnect = false;
|
||||||
saved.error = None;
|
saved.error = None;
|
||||||
|
saved.name = Input::from(saved.name.value.trim());
|
||||||
if form.provider == 0 {
|
if form.provider == 0 {
|
||||||
form.postgres_config()?;
|
form.postgres_config()?;
|
||||||
} else if form.path.value.is_empty() {
|
} else if form.path.value.is_empty() {
|
||||||
@@ -579,12 +598,12 @@ impl Profiles {
|
|||||||
saved.path = Input::from(absolute.to_str().ok_or("SQLite path must be UTF-8")?);
|
saved.path = Input::from(absolute.to_str().ok_or("SQLite path must be UTF-8")?);
|
||||||
}
|
}
|
||||||
self.storage.execute(
|
self.storage.execute(
|
||||||
"INSERT INTO connections (provider, host, port, database_name, username, tls, path, id)
|
"INSERT INTO connections (provider, host, port, database_name, username, tls, path, id, name)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
provider = excluded.provider, host = excluded.host, port = excluded.port,
|
provider = excluded.provider, host = excluded.host, port = excluded.port,
|
||||||
database_name = excluded.database_name, username = excluded.username,
|
database_name = excluded.database_name, username = excluded.username,
|
||||||
tls = excluded.tls, path = excluded.path",
|
tls = excluded.tls, path = excluded.path, name = excluded.name",
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
saved.provider as u8,
|
saved.provider as u8,
|
||||||
saved.postgres[0].value,
|
saved.postgres[0].value,
|
||||||
@@ -593,7 +612,8 @@ impl Profiles {
|
|||||||
saved.postgres[3].value,
|
saved.postgres[3].value,
|
||||||
saved.tls as u8,
|
saved.tls as u8,
|
||||||
saved.path.value,
|
saved.path.value,
|
||||||
saved.id
|
saved.id,
|
||||||
|
saved.name.value
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
saved.id.get_or_insert(self.storage.last_insert_rowid());
|
saved.id.get_or_insert(self.storage.last_insert_rowid());
|
||||||
@@ -609,6 +629,7 @@ impl Profiles {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Form {
|
struct Form {
|
||||||
id: Option<i64>,
|
id: Option<i64>,
|
||||||
|
name: Input,
|
||||||
provider: usize,
|
provider: usize,
|
||||||
focus: usize,
|
focus: usize,
|
||||||
postgres: [Input; 5],
|
postgres: [Input; 5],
|
||||||
@@ -622,6 +643,7 @@ impl Default for Form {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: None,
|
id: None,
|
||||||
|
name: Input::default(),
|
||||||
provider: 0,
|
provider: 0,
|
||||||
focus: 0,
|
focus: 0,
|
||||||
tls: 0,
|
tls: 0,
|
||||||
@@ -634,6 +656,10 @@ impl Default for Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Form {
|
impl Form {
|
||||||
|
fn name_focus(&self) -> usize {
|
||||||
|
if self.provider == 1 { 2 } else { 6 + usize::from(self.reconnect) }
|
||||||
|
}
|
||||||
|
|
||||||
fn connect(&self) -> Result<Database> {
|
fn connect(&self) -> Result<Database> {
|
||||||
if self.provider == 1 {
|
if self.provider == 1 {
|
||||||
if self.path.value.is_empty() {
|
if self.path.value.is_empty() {
|
||||||
@@ -674,6 +700,9 @@ impl Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn input(&self) -> Option<&Input> {
|
fn input(&self) -> Option<&Input> {
|
||||||
|
if self.focus == self.name_focus() {
|
||||||
|
return Some(&self.name);
|
||||||
|
}
|
||||||
if self.provider == 1 {
|
if self.provider == 1 {
|
||||||
(self.focus == 1).then_some(&self.path)
|
(self.focus == 1).then_some(&self.path)
|
||||||
} else {
|
} else {
|
||||||
@@ -684,6 +713,9 @@ impl Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn input_mut(&mut self) -> Option<&mut Input> {
|
fn input_mut(&mut self) -> Option<&mut Input> {
|
||||||
|
if self.focus == self.name_focus() {
|
||||||
|
return Some(&mut self.name);
|
||||||
|
}
|
||||||
if self.provider == 1 {
|
if self.provider == 1 {
|
||||||
(self.focus == 1).then_some(&mut self.path)
|
(self.focus == 1).then_some(&mut self.path)
|
||||||
} else {
|
} else {
|
||||||
@@ -698,11 +730,7 @@ impl Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn edit(&mut self, key: KeyEvent) {
|
fn edit(&mut self, key: KeyEvent) {
|
||||||
let count = if self.provider == 0 {
|
let count = self.name_focus() + 1;
|
||||||
6 + usize::from(self.reconnect)
|
|
||||||
} else {
|
|
||||||
2
|
|
||||||
};
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Tab => self.focus = (self.focus + 1) % count,
|
KeyCode::Tab => self.focus = (self.focus + 1) % count,
|
||||||
KeyCode::BackTab => self.focus = (self.focus + count - 1) % count,
|
KeyCode::BackTab => self.focus = (self.focus + count - 1) % count,
|
||||||
@@ -724,6 +752,13 @@ impl Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn label(&self) -> String {
|
fn label(&self) -> String {
|
||||||
|
if !self.name.value.trim().is_empty() {
|
||||||
|
return crate::db::display_text(self.name.value.trim());
|
||||||
|
}
|
||||||
|
self.connection_label()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connection_label(&self) -> String {
|
||||||
crate::db::display_text(&if self.provider == 1 {
|
crate::db::display_text(&if self.provider == 1 {
|
||||||
let path = Path::new(&self.path.value);
|
let path = Path::new(&self.path.value);
|
||||||
path.file_name()
|
path.file_name()
|
||||||
@@ -787,7 +822,7 @@ impl Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame, screen: Rect) {
|
fn draw(&self, frame: &mut Frame, screen: Rect) {
|
||||||
let minimum_height = if self.provider == 0 { 20 } else { 16 };
|
let minimum_height = if self.provider == 0 { 23 } else { 19 };
|
||||||
if screen.width < 74 || screen.height < minimum_height {
|
if screen.width < 74 || screen.height < minimum_height {
|
||||||
frame.render_widget(Clear, screen);
|
frame.render_widget(Clear, screen);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
@@ -829,6 +864,8 @@ impl Form {
|
|||||||
.style(Style::default().fg(Color::Cyan)),
|
.style(Style::default().fg(Color::Cyan)),
|
||||||
header,
|
header,
|
||||||
);
|
);
|
||||||
|
let [name_area, body] = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).areas(body);
|
||||||
|
self.name.draw(frame, name_area, " Name (optional) ", false, self.focus == self.name_focus(), None);
|
||||||
let [providers, details] =
|
let [providers, details] =
|
||||||
Layout::horizontal([Constraint::Length(24), Constraint::Min(1)]).areas(body);
|
Layout::horizontal([Constraint::Length(24), Constraint::Min(1)]).areas(body);
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
@@ -1526,6 +1563,8 @@ mod tests {
|
|||||||
form.edit(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
form.edit(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||||
assert!(form.input().is_none());
|
assert!(form.input().is_none());
|
||||||
form.edit(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
form.edit(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||||
|
form.edit(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||||
|
form.input_mut().unwrap().paste("Warehouse");
|
||||||
for value in ["db.example", "5440", "warehouse", "inspector"] {
|
for value in ["db.example", "5440", "warehouse", "inspector"] {
|
||||||
form.edit(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
form.edit(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||||
if form.focus == 0 {
|
if form.focus == 0 {
|
||||||
@@ -1543,6 +1582,8 @@ mod tests {
|
|||||||
assert_eq!(profiles.entries.len(), 1);
|
assert_eq!(profiles.entries.len(), 1);
|
||||||
let restored = &profiles.entries[0];
|
let restored = &profiles.entries[0];
|
||||||
assert_eq!(restored.tls, 2);
|
assert_eq!(restored.tls, 2);
|
||||||
|
assert_eq!(restored.label(), "Warehouse");
|
||||||
|
assert_eq!(restored.connection_label(), "warehouse · inspector@db.example:5440");
|
||||||
for (input, expected) in
|
for (input, expected) in
|
||||||
restored
|
restored
|
||||||
.postgres
|
.postgres
|
||||||
@@ -1559,6 +1600,28 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_names_persist_and_blank_names_restore_the_default_label() -> Result<()> {
|
||||||
|
let directory = tempfile::tempdir()?;
|
||||||
|
let path = directory.path().join("connections.sqlite3");
|
||||||
|
let mut profiles = Profiles::open(&path)?;
|
||||||
|
let mut form = Form { provider: 1, path: Input::from("/tmp/example.sqlite"), ..Form::default() };
|
||||||
|
form.focus = form.name_focus();
|
||||||
|
form.input_mut().unwrap().paste(" Local data ");
|
||||||
|
profiles.save(&form)?;
|
||||||
|
drop(profiles);
|
||||||
|
let mut profiles = Profiles::open(&path)?;
|
||||||
|
assert_eq!(profiles.entries[0].label(), "Local data");
|
||||||
|
let mut form = profiles.entries[0].clone();
|
||||||
|
form.name = Input::from(" \t ");
|
||||||
|
profiles.save(&form)?;
|
||||||
|
drop(profiles);
|
||||||
|
let profiles = Profiles::open(&path)?;
|
||||||
|
assert_eq!(profiles.entries.len(), 1);
|
||||||
|
assert_eq!(profiles.entries[0].label(), "example.sqlite");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_ports_are_rejected_before_connecting() {
|
fn invalid_ports_are_rejected_before_connecting() {
|
||||||
for port in ["0", "65536", "not-a-port"] {
|
for port in ["0", "65536", "not-a-port"] {
|
||||||
|
|||||||
+4
-1
@@ -32,7 +32,7 @@ Expansion never runs shell commands. Quote CLI paths to let dbdash expand them.
|
|||||||
PostgreSQL URLs and libpq-style key=value strings are accepted.
|
PostgreSQL URLs and libpq-style key=value strings are accepted.
|
||||||
Use sslmode=require for mandatory TLS; certificates and hostnames are verified.
|
Use sslmode=require for mandatory TLS; certificates and hostnames are verified.
|
||||||
Use sslmode=disable only for trusted local connections.
|
Use sslmode=disable only for trusted local connections.
|
||||||
The inspector needs 70×17 terminal cells; PostgreSQL/SQLite windows need 74×22/74×18.
|
The inspector needs 70×17 terminal cells; PostgreSQL/SQLite windows need 74×25/74×21.
|
||||||
|
|
||||||
Navigation:
|
Navigation:
|
||||||
? Show keybindings in Normal mode (Esc/Enter/? closes)
|
? Show keybindings in Normal mode (Esc/Enter/? closes)
|
||||||
@@ -79,6 +79,9 @@ Navigation:
|
|||||||
New database window: Up/Down chooses a provider; Enter starts editing.
|
New database window: Up/Down chooses a provider; Enter starts editing.
|
||||||
Tab/Shift-Tab changes fields; Left/Right changes TLS when that field is focused.
|
Tab/Shift-Tab changes fields; Left/Right changes TLS when that field is focused.
|
||||||
Enter saves without connecting, including when the database is offline.
|
Enter saves without connecting, including when the database is offline.
|
||||||
|
Optional Name controls the sidebar label; leave it blank for the default label.
|
||||||
|
e on a saved database or its child edits the configuration without connecting.
|
||||||
|
Enter saves edits; Esc discards them. Connected sessions keep their actual identity.
|
||||||
Select a saved database and press Enter to connect. PostgreSQL opens an editable
|
Select a saved database and press Enter to connect. PostgreSQL opens an editable
|
||||||
connection window, initially focused on the password. Tab/Shift-Tab changes fields;
|
connection window, initially focused on the password. Tab/Shift-Tab changes fields;
|
||||||
Left/Right changes TLS when focused. Enter connects and updates that saved entry
|
Left/Right changes TLS when focused. Enter connects and updates that saved entry
|
||||||
|
|||||||
Reference in New Issue
Block a user