Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 66 additions & 8 deletions frontend/src/app/components/connect-db/connect-db.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,19 +205,32 @@
padding: 8px 12px;
}

.connectForm__connectionString {
.connectForm__connectionStringContainer {
background-color: var(--info-background-color);
border: 1px solid var(--color-infoPalette-500);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}

.connectForm__connectionStringHeader {
display: flex;
align-items: flex-start;
gap: 12px;
align-items: center;
gap: 8px;
margin-bottom: 6px;
color: var(--color-infoPalette-500);
}

.connectForm__connectionString ::ng-deep .mat-mdc-text-field-wrapper {
padding-right: 80px;
.connectForm__connectionStringIcon {
font-size: 20px;
width: 20px;
height: 20px;
}

.connectForm__connectionString button {
margin-top: 4px;
margin-left: -80px;
.connectForm__connectionStringDescription {
margin: 0 0 12px 0;
font-size: 11px;
opacity: 0.6;
}

.agent-token {
Expand All @@ -231,3 +244,48 @@
margin-top: 4px;
margin-left: 12px;
}

.connectForm__connectionStringParsed {
display: flex;
align-items: flex-start;
gap: 6px;
margin-top: 8px !important;
color: var(--color-successPalette-500);
font-size: 12px;
}

.connectForm__connectionStringParsed-icon {
font-size: 16px;
width: 16px;
height: 16px;
flex-shrink: 0;
margin-top: 1px;
}

.connectForm__connectionStringParsed-label {
font-weight: 500;
margin-right: 4px;
}

.connectForm__connectionStringParsed-value {
word-break: break-all;
opacity: 0.8;
}

.connectForm__connectionStringOverrideNote {
display: flex;
align-items: center;
gap: 6px;
margin-top: -4px !important;
margin-bottom: 0 !important;
color: var(--color-infoPalette-500);
font-size: 13px;
font-weight: 500;
}

.connectForm__connectionStringOverrideNote-icon {
font-size: 16px;
width: 16px;
height: 16px;
flex-shrink: 0;
}
38 changes: 27 additions & 11 deletions frontend/src/app/components/connect-db/connect-db.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,14 @@ <h1 class="mat-h1 connectForm__fullLine">
</div>

@if (db.connectionType === 'direct' && !db.id) {
<div class="connectForm__fullLine connectForm__connectionString">
<div class="connectForm__fullLine connectForm__connectionStringContainer">
<div class="connectForm__connectionStringHeader">
<mat-icon class="connectForm__connectionStringIcon">link</mat-icon>
<span class="mat-subtitle-2">Quick connect with a connection string</span>
</div>
<p class="mat-body-2 connectForm__connectionStringDescription">
Paste your database connection URI below to automatically fill in all the credentials fields. The string is not stored — only the parsed values are used.
</p>
<mat-form-field appearance="outline" style="width: 100%">
<mat-label>Connection string</mat-label>
<input matInput name="connectionString"
Expand All @@ -81,8 +88,8 @@ <h1 class="mat-h1 connectForm__fullLine">
placeholder="e.g. postgresql://user:password@host:5432/dbname"
connectionStringValidator
[disabled]="submitting"
[(ngModel)]="connectionString">
<mat-hint>Paste your database connection URI to auto-fill credentials</mat-hint>
[(ngModel)]="connectionString"
(ngModelChange)="onConnectionStringChange(connectionStringInput)">
@if (connectionStringInput.errors?.invalidConnectionStringFormat) {
<mat-error>Invalid format. Expected: scheme://user:password@host:port/database</mat-error>
}
Expand All @@ -93,13 +100,21 @@ <h1 class="mat-h1 connectForm__fullLine">
<mat-error>Failed to parse connection string</mat-error>
}
</mat-form-field>
<button type="button" mat-button color="primary"
class="connectForm__connectionStringApplyButton"
data-testid="connection-string-apply-button"
[disabled]="submitting || !connectionString.trim() || connectionStringInput.invalid"
(click)="applyConnectionString()">
Apply
</button>
@if (parsedConnectionString) {
<p class="connectForm__connectionStringParsed">
<mat-icon class="connectForm__connectionStringParsed-icon">check_circle</mat-icon>
<span>
<span class="connectForm__connectionStringParsed-label">Parsed:</span>
<code class="connectForm__connectionStringParsed-value">{{ parsedConnectionString }}</code>
</span>
</p>
}
@if (fieldsOverridden) {
<p class="connectForm__connectionStringOverrideNote">
<mat-icon class="connectForm__connectionStringOverrideNote-icon">info</mat-icon>
Fields are now the source of truth — paste a new connection string to override them
</p>
}
</div>
}

Expand All @@ -111,7 +126,8 @@ <h1 class="mat-h1 connectForm__fullLine">
submitting: submitting,
accessLevel: accessLevel,
masterKey: masterKey,
readonly: !!((accessLevel === 'readonly' || db.isTestConnection) && db.id)
readonly: !!((accessLevel === 'readonly' || db.isTestConnection) && db.id),
autoFilledFields: autoFilledFields
}"
[ndcDynamicOutputs]="credentialsFormOutputs"
[ndcDynamicAttributes]="credentialsFormAttributes"
Expand Down
149 changes: 149 additions & 0 deletions frontend/src/app/components/connect-db/connect-db.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,153 @@ describe('ConnectDBComponent', () => {
},
});
});

describe('Connection string functionality', () => {
it('should parse a PostgreSQL connection string and populate db fields', () => {
component.connectionString = 'postgresql://myuser:mypass@db.example.com:5432/mydb';
component.onConnectionStringChange(null);

expect(component.db.type).toBe(DBtype.Postgres);
expect(component.db.host).toBe('db.example.com');
expect(component.db.port).toBe('5432');
expect(component.db.username).toBe('myuser');
expect(component.db.password).toBe('mypass');
expect(component.db.database).toBe('mydb');
});

it('should parse a MySQL connection string and populate db fields', () => {
component.connectionString = 'mysql://root:secret@localhost:3306/app_db';
component.onConnectionStringChange(null);

expect(component.db.type).toBe(DBtype.MySQL);
expect(component.db.host).toBe('localhost');
expect(component.db.port).toBe('3306');
expect(component.db.username).toBe('root');
expect(component.db.password).toBe('secret');
expect(component.db.database).toBe('app_db');
});

it('should parse a MongoDB connection string with authSource option', () => {
component.connectionString = 'mongodb://admin:pass123@mongo.host.com:27017/mydb?authSource=admin';
component.onConnectionStringChange(null);

expect(component.db.type).toBe(DBtype.Mongo);
expect(component.db.host).toBe('mongo.host.com');
expect(component.db.port).toBe('27017');
expect(component.db.username).toBe('admin');
expect(component.db.password).toBe('pass123');
expect(component.db.database).toBe('mydb');
expect(component.db.authSource).toBe('admin');
expect(component.autoFilledFields.has('authSource')).toBe(true);
});

it('should set autoFilledFields for all parsed fields', () => {
component.connectionString = 'postgresql://user:pass@host:5432/db';
component.onConnectionStringChange(null);

expect(component.autoFilledFields.has('host')).toBe(true);
expect(component.autoFilledFields.has('port')).toBe(true);
expect(component.autoFilledFields.has('username')).toBe(true);
expect(component.autoFilledFields.has('password')).toBe(true);
expect(component.autoFilledFields.has('database')).toBe(true);
});

it('should set ssl to true when sslmode=require is in connection string', () => {
component.connectionString = 'postgresql://user:pass@host:5432/db?sslmode=require';
component.onConnectionStringChange(null);

expect(component.db.ssl).toBe(true);
});

it('should set schema when schema option is present', () => {
component.connectionString = 'postgresql://user:pass@host:5432/db?schema=my_schema';
component.onConnectionStringChange(null);

expect(component.db.schema).toBe('my_schema');
expect(component.autoFilledFields.has('schema')).toBe(true);
});

it('should reset fieldsOverridden to false after parsing', () => {
component.fieldsOverridden = true;
component.connectionString = 'postgresql://user:pass@host:5432/db';
component.onConnectionStringChange(null);

expect(component.fieldsOverridden).toBe(false);
});

it('should not modify db fields when connection string is empty', () => {
const originalType = component.db.type;
const originalHost = component.db.host;

component.connectionString = ' ';
component.onConnectionStringChange(null);

expect(component.db.type).toBe(originalType);
expect(component.db.host).toBe(originalHost);
});

it('should not modify db fields when connection string is invalid', () => {
const originalType = component.db.type;
const originalHost = component.db.host;

component.connectionString = 'not-a-valid-connection-string';
component.onConnectionStringChange(null);

expect(component.db.type).toBe(originalType);
expect(component.db.host).toBe(originalHost);
});

it('should use default port when port is not specified in connection string', () => {
component.connectionString = 'postgresql://user:pass@host/db';
component.onConnectionStringChange(null);

expect(component.db.port).toBe('5432');
});

it('should handle URL-encoded username and password', () => {
component.connectionString = 'postgresql://my%40user:p%40ss%23word@host:5432/db';
component.onConnectionStringChange(null);

expect(component.db.username).toBe('my@user');
expect(component.db.password).toBe('p@ss#word');
});

it('should clear the connection string after successful parsing', () => {
vi.useFakeTimers();
component.connectionString = 'postgresql://user:pass@host:5432/db';
component.onConnectionStringChange(null);

vi.advanceTimersByTime(300);
expect(component.connectionString).toBe('');
vi.useRealTimers();
});

it('should store parsed connection string in parsedConnectionString', () => {
const connStr = 'postgresql://user:pass@host:5432/db';
component.connectionString = connStr;
component.onConnectionStringChange(null);

expect(component.parsedConnectionString).toBe(connStr);
});
});

describe('clearAutoFilledField', () => {
it('should remove the field from autoFilledFields', () => {
component.autoFilledFields = new Set(['host', 'port', 'username']);
component.clearAutoFilledField('host');

expect(component.autoFilledFields.has('host')).toBe(false);
expect(component.autoFilledFields.has('port')).toBe(true);
expect(component.autoFilledFields.has('username')).toBe(true);
});

it('should set fieldsOverridden to true', () => {
component.autoFilledFields = new Set(['host']);
component.fieldsOverridden = false;

component.clearAutoFilledField('host');

expect(component.fieldsOverridden).toBe(true);
});
});
});
20 changes: 17 additions & 3 deletions frontend/src/app/components/connect-db/connect-db.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ export class ConnectDBComponent implements OnInit {
};

public connectionString: string = '';
public parsedConnectionString: string = '';
public autoFilledFields: Set<string> = new Set();
public fieldsOverridden: boolean = false;

public credentialsFormMap: Record<string, Type<BaseCredentialsFormComponent>> = {
[DBtype.MySQL]: MysqlCredentialsFormComponent,
Expand Down Expand Up @@ -136,6 +139,7 @@ export class ConnectDBComponent implements OnInit {
public credentialsFormOutputs: Record<string, any> = {
switchToAgent: () => this.switchToAgent(),
masterKeyChange: (key: string) => this.handleMasterKeyChange(key),
fieldChange: (field: string) => this.clearAutoFilledField(field),
};
public credentialsFormAttributes: Record<string, string> = { class: 'credentials-fieldset' };

Expand Down Expand Up @@ -468,14 +472,17 @@ export class ConnectDBComponent implements OnInit {
this.masterKey = newMasterKey;
}

applyConnectionString() {
onConnectionStringChange(_input: any) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how the handler is called from templates to choose the narrowest input type.
rg -nP --type=html -C2 'onConnectionStringChange\s*\(([^)]*)\)'

Repository: rocket-admin/rocketadmin

Length of output: 805


🏁 Script executed:

#!/bin/bash
# Read the component file to understand the method implementation and return type
cat -n frontend/src/app/components/connect-db/connect-db.component.ts | head -500 | tail -50

Repository: rocket-admin/rocketadmin

Length of output: 1723


🏁 Script executed:

#!/bin/bash
# Search for the method definition and its full signature
rg -n 'onConnectionStringChange' frontend/src/app/components/connect-db/connect-db.component.ts -A 10

Repository: rocket-admin/rocketadmin

Length of output: 461


🏁 Script executed:

#!/bin/bash
# Also check the template file more thoroughly for context
cat -n frontend/src/app/components/connect-db/connect-db.component.html | sed -n '85,100p'

Repository: rocket-admin/rocketadmin

Length of output: 1298


🏁 Script executed:

#!/bin/bash
# Search for all calls to onConnectionStringChange to see if it's called programmatically
rg -n 'onConnectionStringChange' frontend/src/app/components/connect-db/ --type=ts -A 2 -B 2

Repository: rocket-admin/rocketadmin

Length of output: 498


🏁 Script executed:

#!/bin/bash
# Search across entire codebase for programmatic calls
rg -n 'onConnectionStringChange\s*\(' --type=ts -A 1 -B 1

Repository: rocket-admin/rocketadmin

Length of output: 350


🏁 Script executed:

#!/bin/bash
# Get the full method implementation to see where it ends
sed -n '475,525p' frontend/src/app/components/connect-db/connect-db.component.ts

Repository: rocket-admin/rocketadmin

Length of output: 1796


Add explicit return type and remove unused parameter.

The _input parameter is never used in the method body (the method relies on this.connectionString instead). Remove the unused parameter and add an explicit void return type.

Proposed fix
-	onConnectionStringChange(_input: any) {
+	onConnectionStringChange(): void {

Per coding guidelines: "Always add type annotations to function parameters and return types in TypeScript" and "Avoid any types."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onConnectionStringChange(_input: any) {
onConnectionStringChange(): void {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/components/connect-db/connect-db.component.ts` at line 475,
The onConnectionStringChange method currently declares an unused parameter
_input and lacks an explicit return type; remove the unused parameter from
onConnectionStringChange and annotate the method with an explicit void return
type (i.e., change signature to onConnectionStringChange(): void), update any
callers if they pass an argument to stop passing one, and avoid using the any
type anywhere in that signature.

if (!this.connectionString.trim()) {
return;
}

try {
const parsed = parseConnectionString(this.connectionString);

this.parsedConnectionString = this.connectionString;
this.autoFilledFields = new Set(['host', 'port', 'username', 'password', 'database']);
this.fieldsOverridden = false;
this.db.type = parsed.dbType;
this.db.host = parsed.host;
this.db.port = parsed.port;
Expand All @@ -485,21 +492,28 @@ export class ConnectDBComponent implements OnInit {

if (parsed.authSource) {
this.db.authSource = parsed.authSource;
this.autoFilledFields.add('authSource');
}
if (parsed.schema) {
this.db.schema = parsed.schema;
this.autoFilledFields.add('schema');
}
if (parsed.ssl) {
this.db.ssl = true;
}

this.connectionString = '';
this._notifications.showSuccessSnackbar('Connection string parsed successfully');
setTimeout(() => this.connectionString = '', 300);
} catch (_e) {
// Validation directive handles error display
}
}

clearAutoFilledField(field: string): void {
this.autoFilledFields = new Set(this.autoFilledFields);
this.autoFilledFields.delete(field);
this.fieldsOverridden = true;
Comment on lines +511 to +514

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Set fieldsOverridden only when an autofilled field was actually cleared.

fieldsOverridden is currently set to true on every field change, even if no autofilled field existed. That can trigger override UI in manual-entry flows.

Proposed fix
 	clearAutoFilledField(field: string): void {
-		this.autoFilledFields = new Set(this.autoFilledFields);
-		this.autoFilledFields.delete(field);
-		this.fieldsOverridden = true;
+		const hadField = this.autoFilledFields.has(field);
+		this.autoFilledFields = new Set(this.autoFilledFields);
+		this.autoFilledFields.delete(field);
+		if (hadField) {
+			this.fieldsOverridden = true;
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/components/connect-db/connect-db.component.ts` around lines
511 - 514, The clearAutoFilledField method sets fieldsOverridden
unconditionally; change it to only flip to true when an autofilled entry was
actually removed. In clearAutoFilledField(field: string) check whether
this.autoFilledFields contains the field (or capture the boolean result of
delete) before assigning this.autoFilledFields = new
Set(...)/this.autoFilledFields.delete(...) and only set this.fieldsOverridden =
true when the field was present and removed; keep the existing pattern of
creating a new Set to preserve immutability and update only on actual change.

}

getProvider() {
let provider: string = null;
if (this.db.host.endsWith('.amazonaws.com')) provider = 'amazon';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,3 @@ <h1 mat-dialog-title>Give access to our IP address</h1>
I've given access
</button>
</mat-dialog-actions>

Loading
Loading