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
1 change: 1 addition & 0 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export class AppComponent {
this.matIconRegistry.addSvgIcon("cassandra", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/db-logos/сassandra_logo.svg"));
this.matIconRegistry.addSvgIcon("redis", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/db-logos/redis_logo.svg"));
this.matIconRegistry.addSvgIcon("elasticsearch", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/db-logos/elasticsearch_logo.svg"));
this.matIconRegistry.addSvgIcon("clickhouse", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/db-logos/clickhouse_logo.svg"));
this.matIconRegistry.addSvgIcon("github", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/github.svg"));
this.matIconRegistry.addSvgIcon("google", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/google.svg"));
this.matIconRegistry.addSvgIcon("ai_rocket", this.domSanitizer.bypassSecurityTrustResourceUrl("/assets/icons/ai-rocket.svg"));
Expand Down
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 @@ -193,6 +193,20 @@ <h1 class="mat-h1 connectForm__fullLine">
(masterKeyChange)="handleMasterKeyChange($event)">
</app-elastic-credentials-form>

<app-clickhouse-credentials-form *ngIf="db.type === 'clickhouse' && 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-clickhouse-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 @@ -47,6 +47,7 @@ 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 { ClickhouseCredentialsFormComponent } from './db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component';
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

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

The import statement should use ClickHouseCredentialsFormComponent (with capital 'H') to match the ClickHouse branding and be consistent with the component naming convention.

Copilot uses AI. Check for mistakes.

@Component({
selector: 'app-connect-db',
Expand Down Expand Up @@ -76,6 +77,7 @@ import isIP from 'validator/lib/isIP';
PostgresCredentialsFormComponent,
RedisCredentialsFormComponent,
ElasticCredentialsFormComponent,
ClickhouseCredentialsFormComponent,
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

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

The component reference in the imports array should be ClickHouseCredentialsFormComponent (with capital 'H') to match the ClickHouse branding and be consistent with the component naming convention.

Copilot uses AI. Check for mistakes.
IpAddressButtonComponent,
AlertComponent,
Angulartics2Module
Expand Down Expand Up @@ -107,6 +109,7 @@ export class ConnectDBComponent implements OnInit {
[DBtype.Cassandra]: '9042',
[DBtype.Redis]: '6379',
[DBtype.Elasticsearch]: '9200',
[DBtype.ClickHouse]: '8443',
[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,180 @@
<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]="readonly"
[disabled]="submitting"
[(ngModel)]="connection.host">
<mat-hint>
E.g. <strong><code>my-test-db.eu-west-2.aws.clickhouse.cloud</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]="readonly"
[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]="readonly"
[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-error *ngIf="email.errors.required && (email.invalid && email.touched)">Email should not be empty</mat-error> -->
</mat-form-field>

<mat-form-field appearance="outline" class="credentials-fieldset__1-4-columns">
<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"
required
[readonly]="readonly"
[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-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-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"
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

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

The template reference variable should be #ssl="ngModel" instead of #ssh="ngModel" since this is the SSL checkbox, not SSH. This is a copy-paste error that could cause issues with form validation.

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,40 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ClickhouseCredentialsFormComponent } from './clickhouse-credentials-form.component';
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

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

The import statement should use ClickHouseCredentialsFormComponent (with capital 'H') to match the ClickHouse branding and be consistent with the component naming convention.

Copilot uses AI. Check for mistakes.
import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { Angulartics2Module } from 'angulartics2';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideHttpClient } from '@angular/common/http';

describe('ClickhouseCredentialsFormComponent', () => {
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

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

The component class name in the describe block should be ClickHouseCredentialsFormComponent (with capital 'H') to match the ClickHouse branding and be consistent with the component naming convention.

Suggested change
describe('ClickhouseCredentialsFormComponent', () => {
describe('ClickHouseCredentialsFormComponent', () => {

Copilot uses AI. Check for mistakes.
let component: ClickhouseCredentialsFormComponent;
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

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

The component type reference should be ClickHouseCredentialsFormComponent (with capital 'H') to match the ClickHouse branding and be consistent with the component naming convention.

Copilot uses AI. Check for mistakes.
let fixture: ComponentFixture<ClickhouseCredentialsFormComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
FormsModule,
MatCheckboxModule,
BrowserAnimationsModule,
Angulartics2Module.forRoot({}),
ClickhouseCredentialsFormComponent
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

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

The component reference should be ClickHouseCredentialsFormComponent (with capital 'H') to match the ClickHouse branding and be consistent with the component naming convention.

Copilot uses AI. Check for mistakes.
],
providers: [provideHttpClient()]
})
.compileComponents();

fixture = TestBed.createComponent(ClickhouseCredentialsFormComponent);
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

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

The component type reference should be ClickHouseCredentialsFormComponent (with capital 'H') to match the ClickHouse branding and be consistent with the component naming convention.

Copilot uses AI. Check for mistakes.
component = fixture.componentInstance;

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

fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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-clickhouse-credentials-form',
templateUrl: './clickhouse-credentials-form.component.html',
styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './clickhouse-credentials-form.component.css'],
standalone: true,
imports: [
NgIf,
FormsModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatExpansionModule,
HostnameValidationDirective,
MasterEncryptionPasswordComponent,
Angulartics2Module
]
})
export class ClickhouseCredentialsFormComponent extends BaseCredentialsFormComponent {
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

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

The class name should be ClickHouseCredentialsFormComponent (with capital 'H') to match the ClickHouse branding and be consistent with the DBtype.ClickHouse enum value defined in the connection model.

Suggested change
export class ClickhouseCredentialsFormComponent extends BaseCredentialsFormComponent {
export class ClickHouseCredentialsFormComponent extends BaseCredentialsFormComponent {

Copilot uses AI. Check for mistakes.

}
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export class DbTableRowEditComponent implements OnInit {
.filter((field: TableField) => !this.getModifyingFields(res.structure).some(modifyingField => field.column_name === modifyingField.column_name))
.map((field: TableField) => field.column_name);
this.readonlyFields = [...res.readonly_fields, ...this.nonModifyingFields];
if (this.connectionType === DBtype.Dynamo) {
if (this.connectionType === DBtype.Dynamo || this.connectionType === DBtype.ClickHouse) {
this.readonlyFields = [...this.readonlyFields, ...res.primaryColumns.map((field: TableField) => field.column_name)];
}
this.tableForeignKeys = res.foreignKeys;
Expand Down Expand Up @@ -584,7 +584,7 @@ export class DbTableRowEditComponent implements OnInit {
//end crutch

// don't ovverride primary key fields for dynamoDB
if (this.connectionType === DBtype.Dynamo) {
if (this.connectionType === DBtype.Dynamo || this.connectionType === DBtype.ClickHouse) {
const primaryKeyFields = Object.keys(this.keyAttributesFromURL);
primaryKeyFields.forEach((field) => {
delete updatedRow[field];
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/consts/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const supportedOrderedDatabases = [
"mssql",
"redis",
"elasticsearch",
"clickhouse",
"ibmdb2"
]

Expand All @@ -21,5 +22,6 @@ export const supportedDatabasesTitles = {
mssql: "SQL Server",
redis: "Redis",
elasticsearch: "Elasticsearch",
clickhouse: "ClickHouse",
ibmdb2: "IBM DB2"
}
15 changes: 15 additions & 0 deletions frontend/src/app/consts/record-edit-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,5 +327,20 @@ export const recordEditTypes = {
object: JsonEditorEditComponent,
array: JsonEditorEditComponent,
binary: FileEditComponent,
},
clickhouse: {
string: TextEditComponent,
uuid: UuidEditComponent,
boolean: BooleanEditComponent,
integer: NumberEditComponent,
bigint: NumberEditComponent,
float: NumberEditComponent,
double: NumberEditComponent,
decimal: NumberEditComponent,
date: DateEditComponent,
datetime: DateTimeEditComponent,
json: JsonEditorEditComponent,
object: JsonEditorEditComponent,
array: JsonEditorEditComponent,
}
}
Loading
Loading