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
14 changes: 14 additions & 0 deletions frontend/src/app/components/connect-db/connect-db.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ <h1 class="mat-h1 connectForm__fullLine">
(masterKeyChange)="handleMasterKeyChange($event)">
</app-mssql-credentials-form>

<app-redis-credentials-form *ngIf="db.type === 'redis' && db.connectionType === 'direct'"
[ngClass]="{
'credentials-fieldset': !db.isTestConnection,
'credentials-fieldset-no-warning': db.isTestConnection || !isSaas
}"
[connection]="db"
[submitting]="submitting"
[accessLevel]="accessLevel"
[masterKey]="masterKey"
[readonly]="(accessLevel === 'readonly' || db.isTestConnection) && db.id"
(switchToAgent)="switchToAgent"
(masterKeyChange)="handleMasterKeyChange($event)">
</app-redis-credentials-form>

<app-db2-credentials-form *ngIf="db.type === 'ibmdb2' && db.connectionType === 'direct'"
[ngClass]="{
'credentials-fieldset': !db.isTestConnection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Angulartics2, Angulartics2Module } from 'angulartics2';
import { Component, NgZone, OnInit } from '@angular/core';
import { Connection, ConnectionType, DBtype, TestConnection } from 'src/app/models/connection';
import { Subscription, take } from 'rxjs';
import { supportedDatabasesTitles, supportedOrderedDatabases } from 'src/app/consts/databases';

import { AccessLevel } from 'src/app/models/user';
import { AlertComponent } from '../ui-components/alert/alert.component';
Expand Down Expand Up @@ -37,14 +38,14 @@ import { NgForm } from '@angular/forms';
import { NotificationsService } from 'src/app/services/notifications.service';
import { OracledbCredentialsFormComponent } from './db-credentials-forms/oracledb-credentials-form/oracledb-credentials-form.component';
import { PostgresCredentialsFormComponent } from './db-credentials-forms/postgres-credentials-form/postgres-credentials-form.component';
import { RedisCredentialsFormComponent } from './db-credentials-forms/redis-credentials-form/redis-credentials-form.component';
import { Router } from '@angular/router';
import { RouterModule } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { UserService } from 'src/app/services/user.service';
import { environment } from 'src/environments/environment';
import googlIPsList from 'src/app/consts/google-IP-addresses';
import isIP from 'validator/lib/isIP';
import { supportedDatabasesTitles, supportedOrderedDatabases } from 'src/app/consts/databases';

@Component({
selector: 'app-connect-db',
Expand Down Expand Up @@ -72,6 +73,7 @@ import { supportedDatabasesTitles, supportedOrderedDatabases } from 'src/app/con
MysqlCredentialsFormComponent,
OracledbCredentialsFormComponent,
PostgresCredentialsFormComponent,
RedisCredentialsFormComponent,
IpAddressButtonComponent,
AlertComponent,
Angulartics2Module
Expand Down Expand Up @@ -101,6 +103,7 @@ export class ConnectDBComponent implements OnInit {
[DBtype.Mongo]: '27017',
[DBtype.Dynamo]: '',
[DBtype.Cassandra]: '9042',
[DBtype.Redis]: '6379',
[DBtype.DB2]: '50000'
}

Expand Down Expand Up @@ -286,8 +289,8 @@ export class ConnectDBComponent implements OnInit {
(credsCorrect as any) = await this._connections.testConnection(this.connectionID, this.db).toPromise();

this.angulartics2.eventTrack.next({
action: `Connect DB: automatic test connection on edit is ${credsCorrect.result ? 'passed' : 'failed'}`,
properties: { errorMessage: credsCorrect.message }
action: `Connect DB: automatic test connection on edit is ${credsCorrect?.result ? 'passed' : 'failed'}`,
properties: { errorMessage: credsCorrect?.message }
});

if ((this.db.connectionType === 'agent' || credsCorrect.result)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.connectForm__hostname,
.connectForm__port {
padding-bottom: 20px;
}

@media (width <= 600px) {
.connectForm__hostname {
padding-bottom: 44px;
}

.connectForm__port {
padding-bottom: 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<mat-form-field appearance="outline" class="connectForm__hostname credentials-fieldset__1-3-columns">
<mat-label>Hostname</mat-label>
<input matInput name="hostname" #hostname="ngModel"
data-testid="connection-hostname-input"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited"
required hostnameValidator="mongodb"
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The hostnameValidator attribute should not specify "mongodb" as the value. Based on other database forms (e.g., MySQL, Cassandra), this should be either hostnameValidator without a value, or hostnameValidator="redis" if Redis requires special hostname validation.

Suggested change
required hostnameValidator="mongodb"
required hostnameValidator

Copilot uses AI. Check for mistakes.
[readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.host">
<mat-hint>
E.g. <strong><code>mongodb+srv://my-test-db.8a8grvb.mongoconnection.net</code></strong><br>
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The hostname example shows a MongoDB connection string instead of a Redis hostname. This should be updated to show a Redis-appropriate example, such as redis.example.com or my-redis-instance.cache.amazonaws.com.

Suggested change
E.g. <strong><code>mongodb+srv://my-test-db.8a8grvb.mongoconnection.net</code></strong><br>
E.g. <strong><code>redis.example.com</code></strong> or <strong><code>my-redis-instance.cache.amazonaws.com</code></strong><br>

Copilot uses AI. Check for mistakes.
Connections from internal IPs (e.g. localhost) are not supported
</mat-hint>

<mat-error *ngIf="hostname.errors?.isLocalhost && hostname.invalid">
To connect a database to an internal IP, use something like <strong>Pinggy</strong>
(<a [href]="tunnelingServiceLink" target="_blank" class="credentials-fieldset__hint-link">here's a guide</a>),
or <button type="button" (click)="switchToAgent.emit()" class="credentials-fieldset__hint-button">click here</button> to connect through an agent
</mat-error>
<mat-error *ngIf="hostname.errors?.isInvalidHostname && hostname.invalid">Hostname is invalid</mat-error>
</mat-form-field>

<mat-form-field appearance="outline" class="connectForm__port">
<mat-label>Port</mat-label>
<input matInput type="number" name="port" #port="ngModel"
data-testid="connection-port-input"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited"
required
[readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.port">
<mat-error *ngIf="port.errors?.required && (port.invalid && port.touched)">Port should not be empty</mat-error>
</mat-form-field>

<mat-form-field appearance="outline" class="credentials-fieldset__1-4-columns">
<mat-label>Password</mat-label>
<input type="password" matInput name="password" #password="ngModel"
data-testid="connection-password-input"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited"
[required]="!connection.id || hostname.touched || port.touched"
[readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.password">
<mat-hint *ngIf="connection.id && (hostname.pristine && port.pristine)">To keep password the same keep this field blank</mat-hint>
<mat-hint *ngIf="connection.id && (hostname.dirty || port.dirty)">Password needed due to hostname/port change</mat-hint>
</mat-form-field>

<mat-expansion-panel class="credentials-fieldset__1-4-columns">
<mat-expansion-panel-header data-testid="connection-advanced-settings-expansion-panel-header">
<mat-panel-title>
Advanced settings
</mat-panel-title>
</mat-expansion-panel-header>

<div class="advanced-settings">
<app-master-encryption-password class="advanced-settings__fullLine"
[masterKey]="masterKey"
[disabled]="accessLevel === 'readonly' || submitting || connection.isTestConnection"
(onMasterKeyChange)="handleMasterKeyChange($event)">
</app-master-encryption-password>

<mat-form-field appearance="outline" class="advanced-settings__fullLine">
<mat-label>Username</mat-label>
<input matInput name="username" #username="ngModel"
data-testid="connection-username-input"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited"
[readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.username">
<mat-error *ngIf="username.errors?.required && (username.invalid && username.touched)">Username should not be empty</mat-error>
</mat-form-field>

<mat-form-field appearance="outline" class="advanced-settings__fullLine">
<mat-label>Database name</mat-label>
<input matInput name="database" #database="ngModel"
data-testid="connection-database-input"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited"
[readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.database">
<mat-error *ngIf="database.errors?.required && (database.invalid && database.touched)">Name should not be empty</mat-error>
</mat-form-field>

<mat-checkbox class="checkbox-line advanced-settings__fullLine" name="ssh" #ssh="ngModel"
data-testid="connection-ssh-checkbox"
labelPosition="after"
angulartics2On="click"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched"
[angularticsProperties]="{'enable': connection.ssh}"
[disabled]="submitting || connection.isTestConnection"
[(ngModel)]="connection.ssh">
Use SSH tunnel
</mat-checkbox>

<mat-form-field *ngIf="connection.ssh" appearance="outline" floatLabel="always" class="advanced-settings__fullLine">
<mat-label>Private SSH key</mat-label>
<textarea matInput resizeToFitContent rows="8" name="privateSSHKey" #privateSSHKey="ngModel"
placeholder="Sensitive — write-only field"
data-testid="connection-ssh-key-textarea"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited"
[required]="connection.ssh && !connection.id" [readonly]="accessLevel === 'readonly' && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.privateSSHKey"
></textarea>
<mat-error *ngIf="privateSSHKey.errors?.required && (privateSSHKey.invalid && privateSSHKey.touched)">Private SSH key should not be empty</mat-error>
</mat-form-field>

<mat-form-field *ngIf="connection.ssh" appearance="outline">
<mat-label>SSH host</mat-label>
<input matInput name="sshHost" #sshHost="ngModel"
data-testid="connection-ssh-host-input"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited"
[required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.sshHost">
<mat-error *ngIf="sshHost.errors?.required && (sshHost.invalid && sshHost.touched)">SSH host should not be empty</mat-error>
</mat-form-field>

<mat-form-field *ngIf="connection.ssh" appearance="outline">
<mat-label>SSH port</mat-label>
<input matInput type="number" name="sshPort" #sshPort="ngModel"
data-testid="connection-ssh-port-input"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited"
[required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.sshPort">
<mat-error *ngIf="sshPort.errors?.required && (sshPort.invalid && sshPort.touched)">SSH port should not be empty</mat-error>
</mat-form-field>

<mat-form-field *ngIf="connection.ssh" appearance="outline" floatLabel="always">
<mat-label>SSH username</mat-label>
<input matInput name="sshUsername" #sshUsername="ngModel"
placeholder="Sensitive — write-only field"
data-testid="connection-ssh-username-input"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited"
[required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.sshUsername">
<mat-error *ngIf="sshUsername.errors?.required && (sshUsername.invalid && sshUsername.touched)">SSH username should not be empty</mat-error>
</mat-form-field>

<mat-checkbox class="checkbox-line advanced-settings__fullLine" name="ssl" #ssh="ngModel"
labelPosition="after"
data-testid="connection-ssl-checkbox"
angulartics2On="click"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched"
[angularticsProperties]="{'enable': connection.ssl}"
[disabled]="submitting || connection.isTestConnection"
[(ngModel)]="connection.ssl">
Check SSL certificate
</mat-checkbox>

<mat-form-field *ngIf="connection.ssl" appearance="outline" class="advanced-settings__fullLine">
<mat-label>SSL certificate</mat-label>
<textarea matInput resizeToFitContent rows="8" name="sslCert" #sslCert="ngModel"
data-testid="connection-ssl-certificate-textarea"
angulartics2On="change"
angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited"
[required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.cert"
></textarea>
<mat-error *ngIf="sslCert.errors?.required && (sslCert.invalid && sslCert.touched)">SSL certificate should not be empty</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { Angulartics2Module } from 'angulartics2';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { RedisCredentialsFormComponent } from './redis-credentials-form.component';
import { provideHttpClient } from '@angular/common/http';

describe('RedisCredentialsFormComponent', () => {
let component: RedisCredentialsFormComponent;
let fixture: ComponentFixture<RedisCredentialsFormComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
FormsModule,
MatCheckboxModule,
BrowserAnimationsModule,
Angulartics2Module.forRoot({}),
RedisCredentialsFormComponent
],
providers: [provideHttpClient()]
})
.compileComponents();

fixture = TestBed.createComponent(RedisCredentialsFormComponent);
component = fixture.componentInstance;

component.connection = {
id: "12345678"
} as any;

// component.connection = {
// "title": "Test connection via SSH tunnel to mySQL",
// "masterEncryption": false,
// "type": DBtype.MySQL,
// "host": "database-2.cvfuxe8nltiq.us-east-2.rds.amazonaws.com",
// "port": "3306",
// "username": "admin",
// "database": "testDB",
// "schema": null,
// "sid": null,
// "id": "9d5f6d0f-9516-4598-91c4-e4fe6330b4d4",
// "ssh": true,
// "sshHost": "3.134.99.192",
// "sshPort": '22',
// "sshUsername": "ubuntu",
// "ssl": false,
// "cert": null,
// "connectionType": ConnectionType.Direct,
// "azure_encryption": false,
// "signing_key": ''
// };
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Angulartics2Module } from 'angulartics2';
import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive';
import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { NgIf } from '@angular/common';

@Component({
selector: 'app-redis-credentials-form',
templateUrl: './redis-credentials-form.component.html',
styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './redis-credentials-form.component.css'],
imports: [
NgIf,
FormsModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatExpansionModule,
HostnameValidationDirective,
MasterEncryptionPasswordComponent,
Angulartics2Module
]
})
export class RedisCredentialsFormComponent extends BaseCredentialsFormComponent {

}
2 changes: 2 additions & 0 deletions frontend/src/app/consts/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const supportedOrderedDatabases = [
"cassandra",
"oracledb",
"mssql",
"redis",
"ibmdb2"
]

Expand All @@ -17,5 +18,6 @@ export const supportedDatabasesTitles = {
cassandra: "Cassandra",
oracledb: "OracleDB",
mssql: "SQL Server",
redis: "Redis",
ibmdb2: "IBM DB2"
}
8 changes: 8 additions & 0 deletions frontend/src/app/consts/record-edit-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,5 +308,13 @@ export const recordEditTypes = {
list: JsonEditorEditComponent,
map: JsonEditorEditComponent,
set: JsonEditorEditComponent,
},
redis: {
string: TextEditComponent,
integer: NumberEditComponent,
decimal: NumberEditComponent,
boolean: BooleanEditComponent,
array: JsonEditorEditComponent,
json: JsonEditorEditComponent,
}
}
Loading
Loading