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 show_form = false;
|
||||||
let mut info: Option<usize> = None;
|
let mut info: Option<usize> = None;
|
||||||
let mut help: Option<u16> = None;
|
let mut help: Option<u16> = None;
|
||||||
|
let mut remove: Option<usize> = None;
|
||||||
loop {
|
loop {
|
||||||
let entries = explorer_entries(
|
let entries = explorer_entries(
|
||||||
profiles.entries.len(),
|
profiles.entries.len(),
|
||||||
@@ -40,7 +41,7 @@ pub fn run(terminal: &mut DefaultTerminal, initial: Option<Database>) -> Result<
|
|||||||
);
|
);
|
||||||
terminal.draw(|frame| {
|
terminal.draw(|frame| {
|
||||||
let [top, workspace, bottom] = shell_areas(frame.area());
|
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
|
None
|
||||||
} else if show_form {
|
} else if show_form {
|
||||||
form.input().map(Input::position)
|
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 {
|
if let Some(offset) = &mut help {
|
||||||
draw_help(frame, workspace, offset);
|
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()? {
|
match event::read()? {
|
||||||
Event::Key(key) if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) => {
|
Event::Key(key) if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) => {
|
||||||
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
return Ok(());
|
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 {
|
if let Some(offset) = &mut help {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('?') => help = None,
|
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();
|
form = Form::default();
|
||||||
show_form = true;
|
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 => {
|
KeyCode::Char('e') if focus == Focus::Explorer => {
|
||||||
let index = selection.selected().and_then(|index| match entries.get(index) {
|
let index = selection.selected().and_then(|index| match entries.get(index) {
|
||||||
Some(ExplorerEntry::Database(Some(index))) => Some(*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 => {
|
Event::Paste(text) if show_form => {
|
||||||
form.error = None;
|
form.error = None;
|
||||||
if let Some(input) = form.input_mut() {
|
if let Some(input) = form.input_mut() {
|
||||||
@@ -624,6 +664,12 @@ impl Profiles {
|
|||||||
}
|
}
|
||||||
Ok(())
|
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)]
|
#[derive(Clone)]
|
||||||
@@ -1622,6 +1668,31 @@ mod tests {
|
|||||||
Ok(())
|
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]
|
#[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"] {
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ Navigation:
|
|||||||
Optional Name controls the sidebar label; leave it blank for the default label.
|
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.
|
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.
|
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
|
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