+ class="connectForm__fullLine connectForm__warningMessage">
warning_amber
@@ -72,156 +71,53 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ @if (db.connectionType === 'direct' && !db.id) {
+
+
+ Connection string
+
+ Paste your database connection URI to auto-fill credentials
+ @if (connectionStringInput.errors?.invalidConnectionStringFormat) {
+ Invalid format. Expected: scheme://user:password@host:port/database
+ }
+ @if (connectionStringInput.errors?.unsupportedScheme) {
+ Unsupported scheme "{{ connectionStringInput.errors?.unsupportedScheme }}"
+ }
+ @if (connectionStringInput.errors?.invalidConnectionString) {
+ Failed to parse connection string
+ }
+
+
+
+ }
-
-
+ @if (db.connectionType === 'direct') {
+
+ }
+
diff --git a/frontend/src/app/components/connect-db/connect-db.component.ts b/frontend/src/app/components/connect-db/connect-db.component.ts
index b225ac880..0fa290dc2 100644
--- a/frontend/src/app/components/connect-db/connect-db.component.ts
+++ b/frontend/src/app/components/connect-db/connect-db.component.ts
@@ -1,6 +1,6 @@
import { CdkCopyToClipboard } from '@angular/cdk/clipboard';
import { CommonModule } from '@angular/common';
-import { Component, NgZone, OnInit } from '@angular/core';
+import { Component, NgZone, OnInit, Type } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
@@ -13,6 +13,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatTooltipModule } from '@angular/material/tooltip';
import { Title } from '@angular/platform-browser';
import { Router, RouterModule } from '@angular/router';
+import { DynamicAttributesDirective, DynamicModule } from 'ng-dynamic-component';
import { Angulartics2, Angulartics2Module } from 'angulartics2';
import * as ipaddr from 'ipaddr.js';
import posthog from 'posthog-js';
@@ -28,6 +29,9 @@ import { NotificationsService } from 'src/app/services/notifications.service';
import { UserService } from 'src/app/services/user.service';
import { environment } from 'src/environments/environment';
import isIP from 'validator/lib/isIP';
+import { ConnectionStringValidatorDirective } from '../../directives/connection-string-validator.directive';
+import { parseConnectionString } from '../../validators/connection-string.validator';
+import { BaseCredentialsFormComponent } from './db-credentials-forms/base-credentials-form/base-credentials-form.component';
import { AlertComponent } from '../ui-components/alert/alert.component';
import { IpAddressButtonComponent } from '../ui-components/ip-address-button/ip-address-button.component';
import { DbConnectionConfirmDialogComponent } from './db-connection-confirm-dialog/db-connection-confirm-dialog.component';
@@ -63,20 +67,12 @@ import { RedisCredentialsFormComponent } from './db-credentials-forms/redis-cred
MatDialogModule,
MatCheckboxModule,
MatSlideToggleModule,
- Db2CredentialsFormComponent,
- DynamodbCredentialsFormComponent,
- CassandraCredentialsFormComponent,
- MongodbCredentialsFormComponent,
- MssqlCredentialsFormComponent,
- MysqlCredentialsFormComponent,
- OracledbCredentialsFormComponent,
- PostgresCredentialsFormComponent,
- RedisCredentialsFormComponent,
- ElasticCredentialsFormComponent,
- ClickhouseCredentialsFormComponent,
IpAddressButtonComponent,
AlertComponent,
Angulartics2Module,
+ ConnectionStringValidatorDirective,
+ DynamicModule,
+ DynamicAttributesDirective,
],
})
export class ConnectDBComponent implements OnInit {
@@ -94,6 +90,22 @@ export class ConnectDBComponent implements OnInit {
message: null,
};
+ public connectionString: string = '';
+
+ public credentialsFormMap: Record> = {
+ [DBtype.MySQL]: MysqlCredentialsFormComponent,
+ [DBtype.Postgres]: PostgresCredentialsFormComponent,
+ [DBtype.Mongo]: MongodbCredentialsFormComponent,
+ [DBtype.Dynamo]: DynamodbCredentialsFormComponent,
+ [DBtype.Cassandra]: CassandraCredentialsFormComponent,
+ [DBtype.Oracle]: OracledbCredentialsFormComponent,
+ [DBtype.MSSQL]: MssqlCredentialsFormComponent,
+ [DBtype.Redis]: RedisCredentialsFormComponent,
+ [DBtype.Elasticsearch]: ElasticCredentialsFormComponent,
+ [DBtype.ClickHouse]: ClickhouseCredentialsFormComponent,
+ [DBtype.DB2]: Db2CredentialsFormComponent,
+ };
+
public supportedOrderedDatabases = supportedOrderedDatabases;
public supportedDatabasesTitles = supportedDatabasesTitles;
public ports = {
@@ -119,6 +131,14 @@ export class ConnectDBComponent implements OnInit {
"This is a DEMO SESSION! It will disappear after you log out. Don't use databases you're actively using or that contain information you wish to retain.",
};
+ public credentialsFormComponent: Type | null = null;
+ public credentialsFormInputs: Record = {};
+ public credentialsFormOutputs: Record = {
+ switchToAgent: () => this.switchToAgent(),
+ masterKeyChange: (key: string) => this.handleMasterKeyChange(key),
+ };
+ public credentialsFormAttributes: Record = { class: 'credentials-fieldset' };
+
constructor(
private _connections: ConnectionsService,
private _notifications: NotificationsService,
@@ -145,6 +165,8 @@ export class ConnectDBComponent implements OnInit {
this.db.port = this.ports[databaseType];
}
+ this.credentialsFormComponent = this.credentialsFormMap[this.db.type] || null;
+
this._connections
.getCurrentConnectionTitle()
.pipe(take(1))
@@ -177,6 +199,7 @@ export class ConnectDBComponent implements OnInit {
dbTypeChange() {
this.db.port = this.ports[this.db.type];
+ this.credentialsFormComponent = this.credentialsFormMap[this.db.type] || null;
}
testConnection() {
@@ -445,6 +468,38 @@ export class ConnectDBComponent implements OnInit {
this.masterKey = newMasterKey;
}
+ applyConnectionString() {
+ if (!this.connectionString.trim()) {
+ return;
+ }
+
+ try {
+ const parsed = parseConnectionString(this.connectionString);
+
+ this.db.type = parsed.dbType;
+ this.db.host = parsed.host;
+ this.db.port = parsed.port;
+ this.db.username = parsed.username;
+ this.db.password = parsed.password;
+ this.db.database = parsed.database;
+
+ if (parsed.authSource) {
+ this.db.authSource = parsed.authSource;
+ }
+ if (parsed.schema) {
+ this.db.schema = parsed.schema;
+ }
+ if (parsed.ssl) {
+ this.db.ssl = true;
+ }
+
+ this.connectionString = '';
+ this._notifications.showSuccessSnackbar('Connection string parsed successfully');
+ } catch (_e) {
+ // Validation directive handles error display
+ }
+ }
+
getProvider() {
let provider: string = null;
if (this.db.host.endsWith('.amazonaws.com')) provider = 'amazon';
diff --git a/frontend/src/app/directives/connection-string-validator.directive.ts b/frontend/src/app/directives/connection-string-validator.directive.ts
new file mode 100644
index 000000000..02bb48a8c
--- /dev/null
+++ b/frontend/src/app/directives/connection-string-validator.directive.ts
@@ -0,0 +1,19 @@
+import { Directive } from '@angular/core';
+import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
+import { connectionStringValidation } from '../validators/connection-string.validator';
+
+@Directive({
+ selector: '[connectionStringValidator][ngModel]',
+ providers: [
+ {
+ provide: NG_VALIDATORS,
+ useExisting: ConnectionStringValidatorDirective,
+ multi: true,
+ },
+ ],
+})
+export class ConnectionStringValidatorDirective implements Validator {
+ validate(control: AbstractControl): ValidationErrors | null {
+ return connectionStringValidation()(control);
+ }
+}
diff --git a/frontend/src/app/validators/connection-string.validator.ts b/frontend/src/app/validators/connection-string.validator.ts
new file mode 100644
index 000000000..37c24ad7d
--- /dev/null
+++ b/frontend/src/app/validators/connection-string.validator.ts
@@ -0,0 +1,130 @@
+import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
+import { ConnectionStringParser } from 'connection-string-parser';
+import { DBtype } from '../models/connection';
+
+export interface ParsedConnectionString {
+ dbType: DBtype;
+ host: string;
+ port: string;
+ username: string;
+ password: string;
+ database: string;
+ authSource?: string;
+ schema?: string;
+ ssl?: boolean;
+}
+
+const schemeToDbType: Record = {
+ mysql: DBtype.MySQL,
+ mariadb: DBtype.MySQL,
+ postgres: DBtype.Postgres,
+ postgresql: DBtype.Postgres,
+ mongodb: DBtype.Mongo,
+ 'mongodb+srv': DBtype.Mongo,
+ mssql: DBtype.MSSQL,
+ sqlserver: DBtype.MSSQL,
+ oracle: DBtype.Oracle,
+ oracledb: DBtype.Oracle,
+ cassandra: DBtype.Cassandra,
+ redis: DBtype.Redis,
+ rediss: DBtype.Redis,
+ elasticsearch: DBtype.Elasticsearch,
+ clickhouse: DBtype.ClickHouse,
+ ibmdb2: DBtype.DB2,
+ db2: DBtype.DB2,
+};
+
+const defaultPorts: Record = {
+ [DBtype.MySQL]: '3306',
+ [DBtype.Postgres]: '5432',
+ [DBtype.Oracle]: '1521',
+ [DBtype.MSSQL]: '1433',
+ [DBtype.Mongo]: '27017',
+ [DBtype.Dynamo]: '',
+ [DBtype.Cassandra]: '9042',
+ [DBtype.Redis]: '6379',
+ [DBtype.Elasticsearch]: '9200',
+ [DBtype.ClickHouse]: '8443',
+ [DBtype.DB2]: '50000',
+};
+
+export function parseConnectionString(connectionString: string): ParsedConnectionString {
+ const schemeMatch = connectionString.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
+ if (!schemeMatch) {
+ throw new Error('invalidFormat');
+ }
+
+ const scheme = schemeMatch[1].toLowerCase();
+ const dbType = schemeToDbType[scheme];
+ if (!dbType) {
+ throw new Error('unsupportedScheme');
+ }
+
+ const parser = new ConnectionStringParser({ scheme, hosts: [] });
+ const parsed = parser.parse(connectionString);
+
+ const result: ParsedConnectionString = {
+ dbType,
+ host: '',
+ port: defaultPorts[dbType],
+ username: '',
+ password: '',
+ database: '',
+ };
+
+ if (parsed.hosts?.length > 0) {
+ result.host = parsed.hosts[0].host || '';
+ if (parsed.hosts[0].port) {
+ result.port = String(parsed.hosts[0].port);
+ }
+ }
+
+ if (parsed.username) {
+ result.username = decodeURIComponent(parsed.username);
+ }
+
+ if (parsed.password) {
+ result.password = decodeURIComponent(parsed.password);
+ }
+
+ if (parsed.endpoint) {
+ result.database = parsed.endpoint;
+ }
+
+ if (parsed.options) {
+ if (parsed.options.authSource) {
+ result.authSource = parsed.options.authSource;
+ }
+ if (parsed.options.schema) {
+ result.schema = parsed.options.schema;
+ }
+ if (parsed.options.ssl === 'true' || parsed.options.sslmode === 'require' || parsed.options.sslmode === 'verify-full') {
+ result.ssl = true;
+ }
+ }
+
+ return result;
+}
+
+export function connectionStringValidation(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ const value = control.value as string;
+ if (!value || !value.trim()) {
+ return null;
+ }
+
+ try {
+ parseConnectionString(value);
+ return null;
+ } catch (e) {
+ if (e.message === 'invalidFormat') {
+ return { invalidConnectionStringFormat: true };
+ }
+ if (e.message === 'unsupportedScheme') {
+ const schemeMatch = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
+ return { unsupportedScheme: schemeMatch?.[1] || true };
+ }
+ return { invalidConnectionString: true };
+ }
+ };
+}
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index ecccd40e2..dde84d26b 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -7255,6 +7255,15 @@ __metadata:
languageName: node
linkType: hard
+"connection-string-parser@npm:^1.0.4":
+ version: 1.0.4
+ resolution: "connection-string-parser@npm:1.0.4"
+ dependencies:
+ tslib: ^2.0.0
+ checksum: 9424a8b58c1f8eb68c51c7c8254a666f20b405659e3c02c07c2b6b975da643c4b1842704a600bb0448e8f78ad8b6c133a2aadad42493462e382af04ef6900267
+ languageName: node
+ linkType: hard
+
"content-disposition@npm:^1.0.0":
version: 1.0.1
resolution: "content-disposition@npm:1.0.1"
@@ -12778,6 +12787,7 @@ __metadata:
chart.js: ^4.5.1
chartjs-adapter-date-fns: ^3.0.0
color-string: ^2.0.1
+ connection-string-parser: ^1.0.4
convert: ^5.12.0
date-fns: ^4.1.0
ipaddr.js: ^2.2.0