Skip to content

Commit 23e182b

Browse files
committed
fix(connections): pre-populate connection url in js component
1 parent c1776f9 commit 23e182b

6 files changed

Lines changed: 224 additions & 102 deletions

File tree

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

Lines changed: 106 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
/**
2+
* @import { FileValue } from './file_input.js';
3+
*
24
* @typedef Flavor
35
* @type {object}
46
* @property {string} label
57
* @property {string} value
68
* @property {string} icon
79
* @property {string} flavor
10+
* @property {string} connection_string
811
*
912
* @typedef ConnectionStatus
1013
* @type {object}
@@ -36,17 +39,22 @@
3639
* @property {boolean} dirty
3740
* @property {boolean} valid
3841
*
42+
* @typedef FieldsCache
43+
* @type {object}
44+
* @property {FileValue} privateKey
45+
*
3946
* @typedef Properties
4047
* @type {object}
4148
* @property {Connection} connection
4249
* @property {Array.<Flavor>} flavors
4350
* @property {boolean} disableFlavor
44-
* @property {(c: Connection, state: FormState) => void} onChange
51+
* @property {FileValue?} cachedPrivateKeyFile
52+
* @property {(c: Connection, state: FormState, cache?: FieldsCache) => void} onChange
4553
*/
4654
import van from '../van.min.js';
4755
import { Button } from './button.js';
4856
import { Alert } from './alert.js';
49-
import { getValue, emitEvent, loadStylesheet, resizeFrameHeightToElement, resizeFrameHeightOnDOMChange, isEqual } from '../utils.js';
57+
import { getValue, emitEvent, loadStylesheet, isEqual } from '../utils.js';
5058
import { Input } from './input.js';
5159
import { Slider } from './slider.js';
5260
import { Checkbox } from './checkbox.js';
@@ -55,7 +63,8 @@ import { maxLength, minLength, sizeLimit } from '../form_validators.js';
5563
import { RadioGroup } from './radio_group.js';
5664
import { FileInput } from './file_input.js';
5765

58-
const { div, hr, span } = van.tags;
66+
const { div, hr, i, span } = van.tags;
67+
const clearSentinel = '<clear>';
5968
const secretsPlaceholder = '<hidden for safety reasons>';
6069
const defaultPorts = {
6170
redshift: '5439',
@@ -76,8 +85,6 @@ const defaultPorts = {
7685
const ConnectionForm = (props, saveButton) => {
7786
loadStylesheet('connectionform', stylesheet);
7887

79-
window.connectionFormConnection = props.connection;
80-
8188
const connection = getValue(props.connection);
8289
const isEditMode = !!connection?.connection_id;
8390
const defaultPort = defaultPorts[connection?.sql_flavor];
@@ -97,27 +104,32 @@ const ConnectionForm = (props, saveButton) => {
97104
const privateKeyPhrase = van.state(connection?.private_key_passphrase);
98105
const httpPath = van.state(connection?.http_path);
99106

107+
const privateKeyFile = van.state(getValue(props.cachedPrivateKeyFile) ?? null);
108+
van.derive(() => {
109+
const fileInputValue = privateKeyFile.val;
110+
if (fileInputValue?.content) {
111+
privateKey.val = fileInputValue.content.split(',')?.[1] ?? '';
112+
}
113+
});
114+
const clearPrivateKeyPhrase = van.state(false);
115+
100116
if (isEditMode) {
101117
connectionPassword.val = '';
102118
privateKey.val = '';
103119
privateKeyPhrase.val = '';
104120
}
105121

106-
const connectionUrl = connection?.url ?? '';
107-
let connectionStringPrefix = van.state('');
108-
let connectionStringSuffix = van.state(connectionUrl);
109-
if (connectionUrl.includes('@')) {
110-
const [prefixPart, sufixPart] = connectionUrl.split('@');
111-
connectionStringPrefix = van.state(prefixPart);
112-
connectionStringSuffix = van.state(sufixPart ?? '');
122+
const flavor = getValue(props.flavors).find(f => f.value === connectionFlavor.val);
123+
const originalURLTemplate = van.state(flavor.connection_string);
124+
const [prefixPart, sufixPart] = originalURLTemplate.val.split('@');
125+
126+
const connectionStringPrefix = van.state(prefixPart);
127+
const connectionStringSuffix = van.state(connection?.url ?? '');
128+
if (!connectionStringSuffix.val) {
129+
connectionStringSuffix.val = formatURL(sufixPart ?? '', connectionHost.val, connectionPort.val, connectionDatabase.val);
113130
}
114131

115132
const updatedConnection = van.derive(() => {
116-
let privateKeyValue = privateKey.val ?? '';
117-
if (privateKeyValue) {
118-
privateKeyValue = privateKeyValue.content?.split(',')?.[1] ?? '';
119-
}
120-
121133
return {
122134
project_code: connection.project_code,
123135
connection_id: connection.connection_id,
@@ -134,8 +146,8 @@ const ConnectionForm = (props, saveButton) => {
134146
connect_by_url: connectByUrl.val ?? false,
135147
url: connectionStringSuffix.val,
136148
connect_by_key: connectByKey.val ?? false,
137-
private_key: privateKeyValue,
138-
private_key_passphrase: privateKeyPhrase.val ?? '',
149+
private_key: privateKey.val ?? '',
150+
private_key_passphrase: clearPrivateKeyPhrase.val ? clearSentinel : (privateKeyPhrase.val ?? ''),
139151
http_path: httpPath.val ?? '',
140152
};
141153
});
@@ -146,7 +158,7 @@ const ConnectionForm = (props, saveButton) => {
146158
const fieldsValidity = validityPerField.val;
147159
const isValid = Object.keys(fieldsValidity).length > 0 &&
148160
Object.values(fieldsValidity).every(v => v);
149-
props.onChange?.(updatedConnection.val, { dirty: dirty.val, valid: isValid });
161+
props.onChange?.(updatedConnection.val, { dirty: dirty.val, valid: isValid }, { privateKey: privateKeyFile.rawVal });
150162
});
151163

152164
const setFieldValidity = (field, validity) => {
@@ -185,12 +197,13 @@ const ConnectionForm = (props, saveButton) => {
185197
connection,
186198
connectByKey,
187199
connectionPassword,
188-
privateKey,
200+
privateKeyFile,
189201
privateKeyPhrase,
202+
clearPrivateKeyPhrase,
190203
(value, state) => {
191204
connectByKey.val = value.connect_by_key;
192205
connectionPassword.val = value.password;
193-
privateKey.val = value.private_key;
206+
privateKeyFile.val = value.private_key;
194207
privateKeyPhrase.val = value.private_key_passphrase;
195208
setFieldValidity('key_pair_form', state.valid);
196209
},
@@ -223,6 +236,20 @@ const ConnectionForm = (props, saveButton) => {
223236
}
224237
});
225238

239+
van.derive(() => {
240+
const connectionHost_ = connectionHost.val;
241+
const connectionPort_ = connectionPort.val;
242+
const connectionDatabase_ = connectionDatabase.val;
243+
const connectionHttpPath_ = httpPath.val;
244+
const urlTemplate = originalURLTemplate.val;
245+
246+
if (!connectByUrl.rawVal && urlTemplate.includes('@')) {
247+
const [originalURLPrefix, originalURLSuffix] = urlTemplate.split('@');
248+
connectionStringPrefix.val = originalURLPrefix;
249+
connectionStringSuffix.val = formatURL(originalURLSuffix, connectionHost_, connectionPort_, connectionDatabase_, connectionHttpPath_);
250+
}
251+
});
252+
226253
return div(
227254
{ class: 'flex-column fx-gap-3 fx-align-stretch', style: 'overflow-y: auto;' },
228255
div(
@@ -237,7 +264,10 @@ const ConnectionForm = (props, saveButton) => {
237264
height: 38,
238265
help: 'Type of database server to connect to. This determines the database driver and SQL dialect that will be used by TestGen.',
239266
testId: 'sql_flavor',
240-
onChange: (value) => connectionFlavor.val = value,
267+
onChange: (value) => {
268+
const flavor = getValue(props.flavors).find(f => f.value === value);
269+
originalURLTemplate.val = flavor.connection_string;
270+
},
241271
}),
242272
Input({
243273
name: 'connection_name',
@@ -340,10 +370,6 @@ const ConnectionForm = (props, saveButton) => {
340370
return '';
341371
}
342372

343-
if (connectionStringPrefix.val === '') {
344-
connectionStringPrefix.val = `${connectionFlavor.rawVal}://<username>:<password>`;
345-
}
346-
347373
return div(
348374
{ class: 'flex-row fx-gap-3 fx-align-stretch' },
349375
Input({
@@ -466,6 +492,7 @@ const KeyPairConnectionForm = (
466492
password,
467493
privateKey,
468494
privateKeyPhrase,
495+
clearPrivateKeyPhrase,
469496
onValueChange,
470497
useSecretsPlaceholder,
471498
) => {
@@ -512,16 +539,41 @@ const KeyPairConnectionForm = (
512539
if (connectByKey.val) {
513540
return div(
514541
{ class: 'flex-column fx-gap-3' },
515-
Input({
516-
name: 'private_key_passphrase',
517-
label: 'Private Key Passphrase',
518-
value: privateKeyPhrase,
519-
height: 38,
520-
type: 'password',
521-
help: 'Passphrase used when creating the private key. Leave empty if the private key is not encrypted.',
522-
placeholder: (useSecretsPlaceholder && connection.private_key_passphrase) ? secretsPlaceholder : '',
523-
onChange: (value, state) => privateKeyPhraseFieldState.val = {value, valid: state.valid},
524-
}),
542+
div(
543+
{ class: 'key-pair-passphrase-field'},
544+
Input({
545+
name: 'private_key_passphrase',
546+
label: 'Private Key Passphrase',
547+
value: privateKeyPhrase,
548+
height: 38,
549+
type: 'password',
550+
help: 'Passphrase used when creating the private key. Leave empty if the private key is not encrypted.',
551+
placeholder: () => (useSecretsPlaceholder && connection.private_key_passphrase && !clearPrivateKeyPhrase.val) ? secretsPlaceholder : '',
552+
onChange: (value, state) => {
553+
if (value) {
554+
clearPrivateKeyPhrase.val = false;
555+
}
556+
privateKeyPhraseFieldState.val = {value, valid: state.valid};
557+
},
558+
}),
559+
() => {
560+
const hasPrivateKeyPhrase = connection.private_key_passphrase || privateKeyPhraseFieldState.val?.value;
561+
if (!hasPrivateKeyPhrase) {
562+
return '';
563+
}
564+
565+
return i(
566+
{
567+
class: 'material-symbols-rounded clickable text-secondary',
568+
onclick: () => {
569+
clearPrivateKeyPhrase.val = true;
570+
privateKeyPhraseFieldState.val = {value: '', valid: true};
571+
},
572+
},
573+
'clear',
574+
);
575+
},
576+
),
525577
FileInput({
526578
name: 'private_key',
527579
label: 'Upload private key (rsa_key.p8)',
@@ -548,8 +600,25 @@ const KeyPairConnectionForm = (
548600
);
549601
};
550602

603+
function formatURL(url, host, port, database, httpPath) {
604+
return url.replace('<host>', host)
605+
.replace('<port>', port)
606+
.replace('<db_name>', database)
607+
.replace('<http_path>', httpPath);
608+
}
609+
551610
const stylesheet = new CSSStyleSheet();
552611
stylesheet.replace(`
612+
.key-pair-passphrase-field {
613+
position: relative;
614+
}
615+
616+
.key-pair-passphrase-field > i {
617+
position: absolute;
618+
top: 26px;
619+
right: 8px;
620+
}
621+
553622
`);
554623

555624
export { ConnectionForm };

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ const FileInput = (options) => {
118118
event.preventDefault();
119119
fileOver.val = true;
120120
},
121-
ondragleave: () => fileOver.val = false,
121+
ondragleave: (event) => {
122+
if (!event.currentTarget.contains(event.relatedTarget)) {
123+
fileOver.val = false;
124+
}
125+
},
122126
ondragover: (event) => event.preventDefault(),
123127
ondrop: (/** @type {DragEvent} */event) => {
124128
event.preventDefault();

testgen/ui/components/frontend/js/components/toggle.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @typedef Properties
33
* @type {object}
44
* @property {string} label
5+
* @property {string?} name
56
* @property {boolean?} checked
67
* @property {function(boolean)?} onChange
78
*/
@@ -14,11 +15,12 @@ const Toggle = (/** @type Properties */ props) => {
1415
loadStylesheet('toggle', stylesheet);
1516

1617
return label(
17-
{ class: 'flex-row fx-gap-2 clickable' },
18+
{ class: 'flex-row fx-gap-2 clickable', 'data-testid': props.name ?? '' },
1819
input({
1920
type: 'checkbox',
2021
role: 'switch',
2122
class: 'tg-toggle--input clickable',
23+
name: props.name ?? '',
2224
checked: props.checked,
2325
onchange: van.derive(() => {
2426
const onChange = props.onChange?.val ?? props.onChange;

testgen/ui/queries/connection_queries.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,32 @@ def get_table_group_names_by_connection(schema: str, connection_ids: list[str])
4242

4343

4444
def edit_connection(schema, connection, encrypted_password, encrypted_private_key, encrypted_private_key_passphrase):
45-
sql = f"""UPDATE {schema}.connections SET
46-
project_code = '{connection["project_code"]}',
47-
sql_flavor = '{connection["sql_flavor"]}',
48-
sql_flavor_code = '{connection["sql_flavor_code"]}',
49-
project_host = '{connection["project_host"]}',
50-
project_port = '{connection["project_port"]}',
51-
project_user = '{connection["project_user"]}',
52-
project_db = '{connection["project_db"]}',
53-
connection_name = '{connection["connection_name"]}',
54-
max_threads = '{connection["max_threads"]}',
55-
max_query_chars = '{connection["max_query_chars"]}',
56-
url = '{connection["url"]}',
57-
connect_by_key = '{connection["connect_by_key"]}',
58-
connect_by_url = '{connection["connect_by_url"]}',
59-
http_path = '{connection["http_path"]}'"""
60-
61-
if encrypted_password:
62-
sql += f""", project_pw_encrypted = '{encrypted_password}' """
63-
64-
if encrypted_private_key:
65-
sql += f""", private_key = '{encrypted_private_key}' """
66-
67-
if encrypted_private_key_passphrase:
68-
sql += f""", private_key_passphrase = '{encrypted_private_key_passphrase}' """
69-
70-
sql += f""" WHERE connection_id = '{connection["connection_id"]}';"""
45+
encrypted_password_value = f"'{encrypted_password}'" if encrypted_password is not None else "null"
46+
encrypted_private_key_value = f"'{encrypted_private_key}'" if encrypted_private_key is not None else "null"
47+
encrypted_passphrase_value = f"'{encrypted_private_key_passphrase}'" if encrypted_private_key_passphrase is not None else "null"
48+
49+
sql = f"""
50+
UPDATE {schema}.connections
51+
SET
52+
project_code = '{connection["project_code"]}',
53+
sql_flavor = '{connection["sql_flavor"]}',
54+
sql_flavor_code = '{connection["sql_flavor_code"]}',
55+
project_host = '{connection["project_host"]}',
56+
project_port = '{connection["project_port"]}',
57+
project_user = '{connection["project_user"]}',
58+
project_db = '{connection["project_db"]}',
59+
connection_name = '{connection["connection_name"]}',
60+
max_threads = '{connection["max_threads"]}',
61+
max_query_chars = '{connection["max_query_chars"]}',
62+
url = '{connection["url"]}',
63+
connect_by_key = '{connection["connect_by_key"]}',
64+
connect_by_url = '{connection["connect_by_url"]}',
65+
http_path = '{connection["http_path"]}',
66+
project_pw_encrypted = {encrypted_password_value},
67+
private_key = {encrypted_private_key_value},
68+
private_key_passphrase = {encrypted_passphrase_value}
69+
WHERE connection_id = '{connection["connection_id"]}';
70+
"""
7171
db.execute_sql(sql)
7272
st.cache_data.clear()
7373

0 commit comments

Comments
 (0)