Skip to content

Commit c1776f9

Browse files
committed
fix(connections): glitches and inconsistencies with the edit form
1 parent b504ec3 commit c1776f9

8 files changed

Lines changed: 100 additions & 338 deletions

File tree

testgen/ui/components/frontend/js/components/connection_form.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ import { FileInput } from './file_input.js';
5858
const { div, hr, span } = van.tags;
5959
const secretsPlaceholder = '<hidden for safety reasons>';
6060
const defaultPorts = {
61-
mssql: '1433',
6261
redshift: '5439',
62+
azure_mssql: '1433',
63+
synapse_mssql: '1433',
64+
mssql: '1433',
6365
postgresql: '5432',
6466
snowflake: '443',
6567
databricks: '443',
@@ -153,6 +155,7 @@ const ConnectionForm = (props, saveButton) => {
153155

154156
const authenticationForms = {
155157
redshift: () => PasswordConnectionForm(
158+
connection,
156159
connectionPassword,
157160
(value, state) => {
158161
connectionPassword.val = value;
@@ -161,6 +164,7 @@ const ConnectionForm = (props, saveButton) => {
161164
isEditMode,
162165
),
163166
mssql: () => PasswordConnectionForm(
167+
connection,
164168
connectionPassword,
165169
(value, state) => {
166170
connectionPassword.val = value;
@@ -169,6 +173,7 @@ const ConnectionForm = (props, saveButton) => {
169173
isEditMode,
170174
),
171175
postgresql: () => PasswordConnectionForm(
176+
connection,
172177
connectionPassword,
173178
(value, state) => {
174179
connectionPassword.val = value;
@@ -177,6 +182,7 @@ const ConnectionForm = (props, saveButton) => {
177182
isEditMode,
178183
),
179184
snowflake: () => KeyPairConnectionForm(
185+
connection,
180186
connectByKey,
181187
connectionPassword,
182188
privateKey,
@@ -191,6 +197,7 @@ const ConnectionForm = (props, saveButton) => {
191197
isEditMode,
192198
),
193199
databricks: () => HttpPathConnectionForm(
200+
connection,
194201
connectionPassword,
195202
httpPath,
196203
(value, state) => {
@@ -204,12 +211,18 @@ const ConnectionForm = (props, saveButton) => {
204211
const authenticationForm = van.derive(() => {
205212
const selectedFlavorCode = connectionFlavor.val;
206213
const flavor = getValue(props.flavors).find(f => f.value === selectedFlavorCode);
207-
208-
connectionPort.val = defaultPorts[flavor.flavor];
209-
210214
return authenticationForms[flavor.flavor]();
211215
});
212216

217+
van.derive(() => {
218+
const selectedFlavorCode = connectionFlavor.val;
219+
const previousFlavorCode = connectionFlavor.oldVal;
220+
const isCustomPort = connectionPort.rawVal !== defaultPorts[previousFlavorCode];
221+
if (selectedFlavorCode !== previousFlavorCode && (!isCustomPort || !connectionPort.rawVal)) {
222+
connectionPort.val = defaultPorts[selectedFlavorCode];
223+
}
224+
});
225+
213226
return div(
214227
{ class: 'flex-column fx-gap-3 fx-align-stretch', style: 'overflow-y: auto;' },
215228
div(
@@ -338,6 +351,7 @@ const ConnectionForm = (props, saveButton) => {
338351
disabled: true,
339352
value: connectionStringPrefix,
340353
height: 38,
354+
width: 255,
341355
name: 'url_prefix',
342356
}),
343357
Input({
@@ -378,7 +392,7 @@ const ConnectionForm = (props, saveButton) => {
378392
);
379393
};
380394

381-
const PasswordConnectionForm = (password, onValueChange, useSecretsPlaceholder) => {
395+
const PasswordConnectionForm = (connection, password, onValueChange, useSecretsPlaceholder) => {
382396
return div(
383397
{ class: 'flex-row fx-gap-3 fx-align-stretch' },
384398
div(
@@ -389,7 +403,7 @@ const PasswordConnectionForm = (password, onValueChange, useSecretsPlaceholder)
389403
value: password,
390404
height: 38,
391405
type: 'password',
392-
placeholder: useSecretsPlaceholder ? secretsPlaceholder : '',
406+
placeholder: (useSecretsPlaceholder && connection.password) ? secretsPlaceholder : '',
393407
onChange: onValueChange,
394408
}),
395409
),
@@ -401,6 +415,7 @@ const PasswordConnectionForm = (password, onValueChange, useSecretsPlaceholder)
401415
};
402416

403417
const HttpPathConnectionForm = (
418+
connection,
404419
password,
405420
httpPath,
406421
onValueChange,
@@ -425,7 +440,7 @@ const HttpPathConnectionForm = (
425440
value: password,
426441
height: 38,
427442
type: 'password',
428-
placeholder: useSecretsPlaceholder ? secretsPlaceholder : '',
443+
placeholder: (useSecretsPlaceholder && connection.password) ? secretsPlaceholder : '',
429444
onChange: (value, state) => passwordFieldState.val = {value, valid: state.valid},
430445
}),
431446
Input({
@@ -446,6 +461,7 @@ const HttpPathConnectionForm = (
446461
};
447462

448463
const KeyPairConnectionForm = (
464+
connection,
449465
connectByKey,
450466
password,
451467
privateKey,
@@ -503,12 +519,13 @@ const KeyPairConnectionForm = (
503519
height: 38,
504520
type: 'password',
505521
help: 'Passphrase used when creating the private key. Leave empty if the private key is not encrypted.',
506-
placeholder: useSecretsPlaceholder ? secretsPlaceholder : '',
522+
placeholder: (useSecretsPlaceholder && connection.private_key_passphrase) ? secretsPlaceholder : '',
507523
onChange: (value, state) => privateKeyPhraseFieldState.val = {value, valid: state.valid},
508524
}),
509525
FileInput({
510526
name: 'private_key',
511527
label: 'Upload private key (rsa_key.p8)',
528+
placeholder: connection.private_key ? 'Drop file here or browse files to replace existing key' : undefined,
512529
value: privateKey,
513530
onChange: (value, state) => privateKeyFieldState.val = {value, valid: state.valid},
514531
validators: [
@@ -524,7 +541,7 @@ const KeyPairConnectionForm = (
524541
value: password,
525542
height: 38,
526543
type: 'password',
527-
placeholder: useSecretsPlaceholder ? secretsPlaceholder : '',
544+
placeholder: (useSecretsPlaceholder && connection.password) ? secretsPlaceholder : '',
528545
onChange: (value, state) => passwordFieldState.val = {value, valid: state.valid},
529546
});
530547
},

testgen/ui/components/frontend/js/components/file_input.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* @typedef Options
1212
* @type {object}
1313
* @property {string} label
14+
* @property {string?} placeholder
1415
* @property {string} name
1516
* @property {string} value
1617
* @property {string?} class
@@ -38,6 +39,7 @@ const FileInput = (options) => {
3839

3940
const value = van.state(getValue(options.value));
4041
const inputId = `file-uploader-${getRandomId()}`;
42+
const fileOver = van.state(false);
4143
const cssClass = van.derive(() => `tg-file-uploader flex-column fx-gap-2 ${getValue(options.class) ?? ''}`)
4244
const showLoading = van.state(false);
4345
const loadingIndicatorProgress = van.state(0);
@@ -108,10 +110,27 @@ const FileInput = (options) => {
108110
options.label,
109111
),
110112
div(
111-
{ class: 'tg-file-uploader--dropzone flex-column clickable' },
113+
{ class: () => `tg-file-uploader--dropzone flex-column clickable ${fileOver.val ? 'on-dragover' : ''}` },
112114
div(
113115
{
114116
onclick: browseFile,
117+
ondragenter: (event) => {
118+
event.preventDefault();
119+
fileOver.val = true;
120+
},
121+
ondragleave: () => fileOver.val = false,
122+
ondragover: (event) => event.preventDefault(),
123+
ondrop: (/** @type {DragEvent} */event) => {
124+
event.preventDefault();
125+
fileOver.val = false;
126+
127+
let files = [...(event.dataTransfer.items ?? [])].filter((item) => item.kind === 'file').map((item) => item.getAsFile());
128+
if (!event.dataTransfer.items) {
129+
files = [...(event.dataTransfer.files ?? [])];
130+
}
131+
132+
loadFile({ target: { files }});
133+
},
115134
},
116135
input({
117136
id: inputId,
@@ -122,7 +141,7 @@ const FileInput = (options) => {
122141
}),
123142
() => value.val
124143
? FileSummary(value.val, unloadFile)
125-
: FileSelectionDropZone(sizeLimit)
144+
: FileSelectionDropZone(options.placeholder ?? 'Drop file here or browse files', sizeLimit)
126145
),
127146
() => showLoading.val
128147
? div({ class: 'tg-file-uploader--loading', style: loadingIndicatorStyle }, '')
@@ -133,16 +152,17 @@ const FileInput = (options) => {
133152

134153
/**
135154
*
155+
* @param {string} placeholder
136156
* @param {number} sizeLimit
137157
* @returns
138158
*/
139-
const FileSelectionDropZone = (sizeLimit) => {
159+
const FileSelectionDropZone = (placeholder, sizeLimit) => {
140160
return div(
141161
{ class: 'flex-row fx-gap-4' },
142162
Icon({size: 48}, 'cloud_upload'),
143163
div(
144164
{ class: 'flex-column fx-gap-1' },
145-
span({}, 'Drop file here or Browse files'),
165+
span({}, placeholder),
146166
span({ class: 'text-secondary text-caption' }, `Limit ${humanReadableSize(sizeLimit)} per file`),
147167
),
148168
);
@@ -180,6 +200,11 @@ stylesheet.replace(`
180200
background: var(--form-field-color);
181201
padding: 16px;
182202
position: relative;
203+
border: 1px transparent dashed;
204+
}
205+
206+
.tg-file-uploader--dropzone.on-dragover {
207+
border-color: var(--primary-color);
183208
}
184209
185210
.tg-file-uploader--dropzone input[type="file"] {

testgen/ui/components/frontend/js/display_utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ function humanReadableSize(bytes) {
5858

5959
for (const [unit, startsAt] of Object.entries(thresholds)) {
6060
if (bytes > startsAt) {
61-
return `${(bytes / startsAt).toFixed(2)}${unit}`;
61+
return `${(bytes / startsAt).toFixed()}${unit}`;
6262
}
6363
}
6464

testgen/ui/services/connection_service.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def form_overwritten_connection_url(connection) -> str:
170170

171171
connection_credentials = {
172172
"flavor": flavor,
173-
"user": "<user>",
173+
"user": "<username>",
174174
"host": connection["project_host"],
175175
"port": connection["project_port"],
176176
"dbname": connection["project_db"],
@@ -188,3 +188,27 @@ def form_overwritten_connection_url(connection) -> str:
188188
connection_string = flavor_service.get_connection_string("<password>")
189189

190190
return connection_string
191+
192+
193+
def get_connection_string_by_flavor(flavors: list[str]) -> str:
194+
result = {}
195+
for flavor in flavors:
196+
db_type = get_db_type(flavor)
197+
flavor_service = get_flavor_service(db_type)
198+
flavor_service.init({
199+
"flavor": flavor,
200+
"user": "<username>",
201+
"host": "<host>",
202+
"port": "<port>",
203+
"dbname": "<db_name>",
204+
"url": None,
205+
"connect_by_url": None,
206+
"connect_by_key": False,
207+
"private_key": None,
208+
"private_key_passphrase": "",
209+
"dbschema": "",
210+
})
211+
result[flavor] = flavor_service.get_connection_string(
212+
"<password>"
213+
).replace("%3E", ">").replace("%3C", "<")
214+
return result
Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import base64
22
import logging
33
import typing
4-
from dataclasses import asdict, dataclass
4+
from dataclasses import asdict, dataclass, field
55

66
import streamlit as st
77
from sqlalchemy.exc import DatabaseError
@@ -16,7 +16,6 @@
1616
from testgen.ui.navigation.page import Page
1717
from testgen.ui.services import connection_service, table_group_service, user_session_service
1818
from testgen.ui.session import session, temp_value
19-
from testgen.ui.views.connections.models import ConnectionStatus
2019
from testgen.utils import format_field
2120

2221
LOG = logging.getLogger("testgen")
@@ -222,11 +221,18 @@ def _format_connection(self, connection: dict, should_test: bool = False) -> dic
222221
if should_test:
223222
formatted_connection["status"] = asdict(self.test_connection(connection))
224223

225-
formatted_connection["password"] = "***" # noqa S105
226-
formatted_connection["private_key"] = "***" # S105
227-
formatted_connection["private_key_passphrase"] = "***" # noqa S105
224+
if formatted_connection["password"]:
225+
formatted_connection["password"] = "***" # noqa S105
226+
if formatted_connection["private_key"]:
227+
formatted_connection["private_key"] = "***" # S105
228+
if formatted_connection["private_key_passphrase"]:
229+
formatted_connection["private_key_passphrase"] = "***" # noqa S105
228230

229-
flavors = [f for f in self.flavor_options if f.value == connection["sql_flavor_code"]]
231+
first_match = [f for f in self.flavor_options if f.flavor == formatted_connection.get("sql_flavor")]
232+
if formatted_connection["sql_flavor"] and not formatted_connection.get("sql_flavor_code") and first_match:
233+
formatted_connection["sql_flavor_code"] = first_match[0].flavor
234+
235+
flavors = [f for f in self.flavor_options if f.value == formatted_connection["sql_flavor_code"]]
230236
if flavors and (flavor := flavors[0]):
231237
formatted_connection["flavor"] = asdict(flavor)
232238

@@ -353,3 +359,10 @@ def on_go_to_profiling_runs(params: dict) -> None:
353359
"GoToProfilingRunsClicked": on_go_to_profiling_runs,
354360
},
355361
)
362+
363+
364+
@dataclass(frozen=True, slots=True)
365+
class ConnectionStatus:
366+
message: str
367+
successful: bool
368+
details: str | None = field(default=None)

testgen/ui/views/connections/__init__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)