feat: remove saved connection configurations safely
This commit is contained in:
+73
-2
@@ -31,6 +31,7 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
||||
let mut show_form = false;
|
||||
let mut info: Option<usize> = None;
|
||||
let mut help: Option<u16> = None;
|
||||
let mut remove: Option<usize> = None;
|
||||
loop {
|
||||
let entries = explorer_entries(
|
||||
profiles.entries.len(),
|
||||
@@ -40,7 +41,7 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
||||
);
|
||||
terminal.draw(|frame| {
|
||||
let [top, workspace, bottom] = shell_areas(frame.area());
|
||||
let position = if info.is_some() || help.is_some() {
|
||||
let position = if info.is_some() || help.is_some() || remove.is_some() {
|
||||
None
|
||||
} else if show_form {
|
||||
form.input().map(Input::position)
|
||||
@@ -93,12 +94,45 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
||||
if let Some(offset) = &mut help {
|
||||
draw_help(frame, workspace, offset);
|
||||
}
|
||||
if let Some(index) = remove {
|
||||
let width = workspace.width.min(70);
|
||||
let height = workspace.height.min(7);
|
||||
let area = Rect::new(workspace.x + (workspace.width - width) / 2,
|
||||
workspace.y + (workspace.height - height) / 2, width, height);
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(Paragraph::new(format!(
|
||||
"Remove saved connection \"{}\"?\nOnly the configuration is removed. The database and active session are kept.\n\ny: remove · n/Esc: cancel",
|
||||
profiles.entries[index].label()))
|
||||
.wrap(Wrap { trim: false }).block(Block::bordered().title(" Remove connection ")), area);
|
||||
}
|
||||
})?;
|
||||
match event::read()? {
|
||||
Event::Key(key) if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) => {
|
||||
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(index) = remove {
|
||||
match key.code {
|
||||
KeyCode::Char('y') => {
|
||||
match profiles.remove(index) {
|
||||
Ok(()) => {
|
||||
active = active.and_then(|active| {
|
||||
if active == index { None } else { Some(active - usize::from(active > index)) }
|
||||
});
|
||||
let updated = explorer_entries(profiles.entries.len(), active,
|
||||
browser.as_ref().map(|app| app.relations.len()), expanded);
|
||||
selection.select((!updated.is_empty()).then(|| selection.selected().unwrap_or(0).min(updated.len() - 1)));
|
||||
error = None;
|
||||
}
|
||||
Err(failure) => error = Some(error_text(failure.as_ref())),
|
||||
}
|
||||
remove = None;
|
||||
}
|
||||
KeyCode::Esc | KeyCode::Char('n') => remove = None,
|
||||
_ => {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(offset) = &mut help {
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('?') => help = None,
|
||||
@@ -207,6 +241,12 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
||||
form = Form::default();
|
||||
show_form = true;
|
||||
}
|
||||
KeyCode::Char('d') | KeyCode::Delete if focus == Focus::Explorer => {
|
||||
remove = selection.selected().and_then(|index| match entries.get(index) {
|
||||
Some(ExplorerEntry::Database(Some(index))) => Some(*index),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
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),
|
||||
@@ -317,7 +357,7 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Paste(_) if info.is_some() || help.is_some() => {}
|
||||
Event::Paste(_) if info.is_some() || help.is_some() || remove.is_some() => {}
|
||||
Event::Paste(text) if show_form => {
|
||||
form.error = None;
|
||||
if let Some(input) = form.input_mut() {
|
||||
@@ -624,6 +664,12 @@ impl Profiles {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&mut self, index: usize) -> Result<()> {
|
||||
self.storage.execute("DELETE FROM connections WHERE id = ?1", [self.entries[index].id])?;
|
||||
self.entries.remove(index);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -1622,6 +1668,31 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_a_profile_keeps_database_files_and_surviving_profiles() -> Result<()> {
|
||||
let directory = tempfile::tempdir()?;
|
||||
let database = directory.path().join("data.sqlite");
|
||||
let contents = b"database file must not be touched";
|
||||
std::fs::write(&database, contents)?;
|
||||
let path = directory.path().join("profiles.sqlite");
|
||||
let mut profiles = Profiles::open(&path)?;
|
||||
let form = Form { provider: 1, path: Input::from(database.to_str().unwrap()), ..Form::default() };
|
||||
profiles.save(&form)?;
|
||||
profiles.save(&form)?;
|
||||
let survivor = profiles.entries[1].id;
|
||||
profiles.storage.execute_batch("PRAGMA query_only = ON")?;
|
||||
assert!(profiles.remove(0).is_err());
|
||||
assert_eq!(profiles.entries.len(), 2);
|
||||
profiles.storage.execute_batch("PRAGMA query_only = OFF")?;
|
||||
profiles.remove(0)?;
|
||||
drop(profiles);
|
||||
let profiles = Profiles::open(&path)?;
|
||||
assert_eq!(profiles.entries.len(), 1);
|
||||
assert_eq!(profiles.entries[0].id, survivor);
|
||||
assert_eq!(std::fs::read(database)?, contents);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_ports_are_rejected_before_connecting() {
|
||||
for port in ["0", "65536", "not-a-port"] {
|
||||
|
||||
@@ -82,6 +82,8 @@ Navigation:
|
||||
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.
|
||||
d / Delete on a saved database asks for y to remove its configuration (n/Esc cancels).
|
||||
Database files are never deleted. Removing the active profile keeps its session open.
|
||||
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;
|
||||
Left/Right changes TLS when focused. Enter connects and updates that saved entry
|
||||
|
||||
Reference in New Issue
Block a user