Skip to content

Commit 2ee6d0b

Browse files
committed
Add hide show pwd and fix edge cases
1 parent 1768217 commit 2ee6d0b

File tree

10 files changed

+826
-15
lines changed

10 files changed

+826
-15
lines changed

sqlit/domains/connections/providers/postgresql/base.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from __future__ import annotations
44

5-
from typing import Any
5+
from dataclasses import replace
6+
from typing import TYPE_CHECKING, Any
67

78
from sqlit.domains.connections.providers.adapters.base import (
89
ColumnInfo,
@@ -13,6 +14,9 @@
1314
TriggerInfo,
1415
)
1516

17+
if TYPE_CHECKING:
18+
from sqlit.domains.connections.domain.config import ConnectionConfig
19+
1620

1721
class PostgresBaseAdapter(CursorBasedAdapter):
1822
"""Base class for PostgreSQL-compatible databases (PostgreSQL, CockroachDB).
@@ -41,6 +45,19 @@ def supports_cross_database_queries(self) -> bool:
4145
def default_schema(self) -> str:
4246
return "public"
4347

48+
def apply_database_override(self, config: ConnectionConfig, database: str) -> ConnectionConfig:
49+
"""Apply database override by modifying the connection config.
50+
51+
PostgreSQL databases are isolated and don't support cross-database queries.
52+
To query a table in another database, we must connect to that database.
53+
This returns a new config with the target database set.
54+
"""
55+
endpoint = config.tcp_endpoint
56+
if endpoint is None:
57+
return config
58+
new_endpoint = replace(endpoint, database=database)
59+
return replace(config, endpoint=new_endpoint)
60+
4461
def get_tables(self, conn: Any, database: str | None = None) -> list[TableInfo]:
4562
"""Get list of tables from all schemas."""
4663
cursor = conn.cursor()

sqlit/domains/connections/ui/connection_focus.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from textual.widgets import Button, Input, OptionList, Select, TabbedContent
99

1010
from sqlit.domains.connections.ui.connection_form import ConnectionFormController
11+
from sqlit.domains.connections.providers.schema_helpers import FieldType
1112
from sqlit.domains.connections.ui.fields import FieldDefinition
1213

1314

@@ -50,6 +51,12 @@ def collect_tab_fields(tab_name: str) -> list[Any]:
5051
collected.append(browse_btn)
5152
except Exception:
5253
pass
54+
if field_def.field_type == FieldType.PASSWORD:
55+
try:
56+
toggle_btn = self._screen.query_one(f"#toggle-password-{name}", Button)
57+
collected.append(toggle_btn)
58+
except Exception:
59+
pass
5360
except Exception:
5461
pass
5562
return collected

sqlit/domains/connections/ui/field_widgets.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,27 @@ def build_field_container(
7070
self._field_definitions[field_def.name] = field_def
7171
container.compose_add_child(option_list)
7272
container.compose_add_child(Static("", id=f"error-{field_def.name}", classes="error-text hidden"))
73+
elif field_def.field_type == FieldType.PASSWORD:
74+
value = self._get_field_value(field_def.name) or field_def.default
75+
input_widget = Input(
76+
value=value,
77+
placeholder=field_def.placeholder,
78+
id=field_id,
79+
password=True,
80+
)
81+
self._field_widgets[field_def.name] = input_widget
82+
self._field_definitions[field_def.name] = field_def
83+
84+
password_row = Horizontal(classes="password-field-row")
85+
password_row.compose_add_child(input_widget)
86+
toggle_btn = Button(
87+
"Show",
88+
id=f"toggle-password-{field_def.name}",
89+
classes="password-toggle-button",
90+
)
91+
password_row.compose_add_child(toggle_btn)
92+
container.compose_add_child(password_row)
93+
container.compose_add_child(Static("", id=f"error-{field_def.name}", classes="error-text hidden"))
7394
elif field_def.field_type in (FieldType.FILE, FieldType.DIRECTORY):
7495
value = self._get_field_value(field_def.name) or field_def.default
7596
input_widget = Input(

sqlit/domains/connections/ui/screens/connection.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,14 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
519519
if event.button.id and event.button.id.startswith("browse-"):
520520
field_name = event.button.id[7:] # Remove "browse-" prefix
521521
self._on_browse_file(field_name)
522+
return
523+
if event.button.id and event.button.id.startswith("toggle-password-"):
524+
field_name = event.button.id[len("toggle-password-") :]
525+
widget = self._form.field_widgets.get(field_name)
526+
if isinstance(widget, Input):
527+
widget.password = not widget.password
528+
event.button.label = "Hide" if not widget.password else "Show"
529+
widget.focus()
522530

523531
def _update_field_visibility(self) -> None:
524532
self._form.update_field_visibility(self._get_field_container)

sqlit/domains/connections/ui/screens/connection_styles.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,24 @@
184184
width: 1fr;
185185
}
186186
187+
.password-field-row {
188+
width: 100%;
189+
height: 1;
190+
}
191+
192+
.password-field-row Input {
193+
width: 1fr;
194+
}
195+
196+
.password-toggle-button {
197+
width: 6;
198+
min-width: 6;
199+
height: 1;
200+
border: none;
201+
margin-left: 1;
202+
padding: 0;
203+
}
204+
187205
.browse-button {
188206
width: 5;
189207
min-width: 5;

sqlit/domains/connections/ui/screens/password_input.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
from textual.app import ComposeResult
88
from textual.binding import Binding
9+
from textual.containers import Container, Horizontal
910
from textual.screen import ModalScreen
10-
from textual.widgets import Input, Static
11+
from textual.widgets import Button, Input, Static
1112

1213
from sqlit.shared.ui.widgets import Dialog
1314

@@ -22,6 +23,8 @@ class PasswordInputScreen(ModalScreen):
2223
BINDINGS = [
2324
Binding("escape", "cancel", "Cancel", priority=True),
2425
Binding("enter", "submit", "Submit", show=False),
26+
Binding("tab", "focus_next", "Next field", show=False, priority=True),
27+
Binding("shift+tab", "focus_prev", "Previous field", show=False, priority=True),
2528
]
2629

2730
CSS = """
@@ -70,6 +73,24 @@ class PasswordInputScreen(ModalScreen):
7073
border: none;
7174
background-tint: $foreground 5%;
7275
}
76+
77+
#password-row {
78+
width: 100%;
79+
height: 1;
80+
}
81+
82+
#password-row Input {
83+
width: 1fr;
84+
}
85+
86+
#password-toggle {
87+
width: 6;
88+
min-width: 6;
89+
height: 1;
90+
border: none;
91+
margin-left: 1;
92+
padding: 0;
93+
}
7394
"""
7495

7596
def __init__(
@@ -106,17 +127,20 @@ def compose(self) -> ComposeResult:
106127
shortcuts: list[tuple[str, str]] = [("Submit", "<enter>"), ("Cancel", "<esc>")]
107128
with Dialog(id="password-dialog", title=self.title_text, shortcuts=shortcuts):
108129
yield Static(self.description, id="password-description")
109-
from textual.containers import Container
110-
111130
container = Container(id="password-container")
112131
container.border_title = "Password"
113132
with container:
114-
yield Input(
115-
value="",
116-
placeholder="",
117-
id="password-input",
118-
password=False,
133+
row = Horizontal(id="password-row")
134+
row.compose_add_child(
135+
Input(
136+
value="",
137+
placeholder="",
138+
id="password-input",
139+
password=True,
140+
)
119141
)
142+
row.compose_add_child(Button("Show", id="password-toggle"))
143+
yield row
120144

121145
def on_mount(self) -> None:
122146
self.query_one("#password-input", Input).focus()
@@ -131,6 +155,25 @@ def on_input_submitted(self, event: Input.Submitted) -> None:
131155
self._log_submit("input_submitted", event.value)
132156
self.dismiss(event.value)
133157

158+
def on_button_pressed(self, event: Button.Pressed) -> None:
159+
if event.button.id != "password-toggle":
160+
return
161+
input_widget = self.query_one("#password-input", Input)
162+
input_widget.password = not input_widget.password
163+
event.button.label = "Hide" if not input_widget.password else "Show"
164+
input_widget.focus()
165+
166+
def action_focus_next(self) -> None:
167+
input_widget = self.query_one("#password-input", Input)
168+
toggle_btn = self.query_one("#password-toggle", Button)
169+
if self.focused is input_widget:
170+
toggle_btn.focus()
171+
else:
172+
input_widget.focus()
173+
174+
def action_focus_prev(self) -> None:
175+
self.action_focus_next()
176+
134177
def on_descendant_focus(self, event: Any) -> None:
135178
try:
136179
container = self.query_one("#password-container")

sqlit/domains/explorer/ui/tree/builder.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,11 @@ def get_conn_label(config: Any, connected: bool = False) -> str:
504504
dbs_node.data = FolderNode(folder_type="databases")
505505
dbs_node.allow_expand = True
506506
active_node.expand()
507+
# Trigger async load of databases so they're visible after refresh
508+
from . import loaders
509+
loaders.add_loading_placeholder(host, dbs_node)
510+
loaders.load_folder_async(host, dbs_node, dbs_node.data)
511+
dbs_node.expand()
507512
else:
508513
add_database_object_nodes(host, active_node, None)
509514
active_node.expand()

0 commit comments

Comments
 (0)