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 @@ -179,6 +179,20 @@ <h1 class="mat-h1 connectForm__fullLine">
(masterKeyChange)="handleMasterKeyChange($event)">
</app-redis-credentials-form>

<app-elastic-credentials-form *ngIf="db.type === 'elasticsearch' && 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-elastic-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 @@ -19,6 +19,7 @@ import { DbConnectionConfirmDialogComponent } from './db-connection-confirm-dial
import { DbConnectionDeleteDialogComponent } from './db-connection-delete-dialog/db-connection-delete-dialog.component';
import { DbConnectionIpAccessDialogComponent } from './db-connection-ip-access-dialog/db-connection-ip-access-dialog.component';
import { DynamodbCredentialsFormComponent } from './db-credentials-forms/dynamodb-credentials-form/dynamodb-credentials-form.component';
import { ElasticCredentialsFormComponent } from './db-credentials-forms/elastic-credentials-form/elastic-credentials-form.component';
import { FormsModule } from '@angular/forms';
import { IpAddressButtonComponent } from '../ui-components/ip-address-button/ip-address-button.component';
import { MatButtonModule } from '@angular/material/button';
Expand Down Expand Up @@ -74,6 +75,7 @@ import isIP from 'validator/lib/isIP';
OracledbCredentialsFormComponent,
PostgresCredentialsFormComponent,
RedisCredentialsFormComponent,
ElasticCredentialsFormComponent,
IpAddressButtonComponent,
AlertComponent,
Angulartics2Module
Expand Down Expand Up @@ -104,6 +106,7 @@ export class ConnectDBComponent implements OnInit {
[DBtype.Dynamo]: '',
[DBtype.Cassandra]: '9042',
[DBtype.Redis]: '6379',
[DBtype.Elasticsearch]: '9200',
[DBtype.DB2]: '50000'
}

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,188 @@
<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
[readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id"
[disabled]="submitting"
[(ngModel)]="connection.host">
<mat-hint>
E.g. <strong><code>cluster-123456.es.europe-west1.gcp.elastic-cloud.com</code></strong><br>
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-2-columns">
<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"
required
[readonly]="readonly"
[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="credentials-fieldset__3-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>

Comment on lines +78 to +89

Copilot AI Nov 21, 2025

Copy link

Choose a reason for hiding this comment

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

Duplicate "Username" field. This field is already defined at lines 38-48 with required validation. The duplicate field in the advanced settings section (lines 78-88) should be removed. For comparison, other credential forms like Postgres only have the username field once in the main form section, not duplicated in advanced settings.

Suggested change
<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>

Copilot uses AI. Check for mistakes.
<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"

Copilot AI Nov 21, 2025

Copy link

Choose a reason for hiding this comment

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

Incorrect variable reference: the variable name should be #ssl but it's currently set to #ssh. This line creates a template reference variable for the SSL checkbox, so it should match the field it's referencing. Change #ssh="ngModel" to #ssl="ngModel".

Suggested change
<mat-checkbox class="checkbox-line advanced-settings__fullLine" name="ssl" #ssh="ngModel"
<mat-checkbox class="checkbox-line advanced-settings__fullLine" name="ssl" #ssl="ngModel"

Copilot uses AI. Check for mistakes.
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 { ElasticCredentialsFormComponent } from './elastic-credentials-form.component';
import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { provideHttpClient } from '@angular/common/http';

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

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

fixture = TestBed.createComponent(ElasticCredentialsFormComponent);
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-elastic-credentials-form',
templateUrl: './elastic-credentials-form.component.html',
styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './elastic-credentials-form.component.css'],
imports: [
NgIf,
FormsModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatExpansionModule,
HostnameValidationDirective,
MasterEncryptionPasswordComponent,
Angulartics2Module
]
})
export class ElasticCredentialsFormComponent 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 @@ -7,6 +7,7 @@ export const supportedOrderedDatabases = [
"oracledb",
"mssql",
"redis",
"elasticsearch",
"ibmdb2"
]

Expand All @@ -19,5 +20,6 @@ export const supportedDatabasesTitles = {
oracledb: "OracleDB",
mssql: "SQL Server",
redis: "Redis",
elasticsearch: "Elasticsearch",
ibmdb2: "IBM DB2"
}
9 changes: 9 additions & 0 deletions frontend/src/app/consts/record-edit-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,5 +316,14 @@ export const recordEditTypes = {
boolean: BooleanEditComponent,
array: JsonEditorEditComponent,
json: JsonEditorEditComponent,
},
elasticsearch: {
string: TextEditComponent,
number: NumberEditComponent,
boolean: BooleanEditComponent,
date: DateEditComponent,
object: JsonEditorEditComponent,
array: JsonEditorEditComponent,
binary: FileEditComponent,
}
}
9 changes: 9 additions & 0 deletions frontend/src/app/consts/record-view-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,5 +309,14 @@ export const recordViewFieldTypes = {
boolean: BooleanRecordViewComponent,
array: JsonEditorRecordViewComponent,
json: JsonEditorRecordViewComponent,
},
elasticsearch: {
string: TextRecordViewComponent,
number: NumberRecordViewComponent,
boolean: BooleanRecordViewComponent,
date: DateRecordViewComponent,
object: JsonEditorRecordViewComponent,
array: JsonEditorRecordViewComponent,
binary: FileRecordViewComponent,
}
}
Loading
Loading