diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index a298447e3..481c5b81a 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -153,7 +153,7 @@ class="nav-bar__upgrade-button" angulartics2On="click" angularticsAction="Demo navbar: Create account is clicked" - (click)="logoutAndRedirectToRegistration()"> + (click)="logoutAndRedirectToRegistration(); posthog.capture('Demo navbar: Create account is clicked')"> Create account diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index be3676613..307789763 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -15,6 +15,7 @@ import { User } from '@sentry/angular'; import amplitude from 'amplitude-js'; import { Angulartics2, Angulartics2Amplitude, Angulartics2OnModule } from 'angulartics2'; import { differenceInMilliseconds } from 'date-fns'; +import posthog from 'posthog-js'; import { Subject } from 'rxjs'; import { filter } from 'rxjs/operators'; import { environment } from '../environments/environment'; @@ -23,6 +24,7 @@ import { Connection } from './models/connection'; import { AuthService } from './services/auth.service'; import { CompanyService } from './services/company.service'; import { ConnectionsService } from './services/connections.service'; +import { PosthogService } from './services/posthog.service'; import { TablesService } from './services/tables.service'; import { UiSettingsService } from './services/ui-settings.service'; import { UserService } from './services/user.service'; @@ -53,6 +55,7 @@ amplitude.getInstance().init('9afd282be91f94da735c11418d5ff4f5'); ], }) export class AppComponent { + protected posthog = posthog; public isSaas = (environment as any).saas; public appVersion = version; userActivity; @@ -95,6 +98,7 @@ export class AppComponent { private angulartics2: Angulartics2, private domSanitizer: DomSanitizer, private matIconRegistry: MatIconRegistry, + _posthog: PosthogService, ) { this.matIconRegistry.addSvgIcon( 'mysql', @@ -167,6 +171,7 @@ export class AppComponent { this.angulartics2.eventTrack.next({ action: 'Demo account is logged in', }); + posthog.capture('Demo account is logged in'); }); } }); diff --git a/frontend/src/app/components/api-keys/api-keys.component.html b/frontend/src/app/components/api-keys/api-keys.component.html index e72126991..0c776eaa3 100644 --- a/frontend/src/app/components/api-keys/api-keys.component.html +++ b/frontend/src/app/components/api-keys/api-keys.component.html @@ -18,6 +18,7 @@

API Keys

data-testid="api-key-input" angulartics2On="change" angularticsAction="API Keys: api key title is edited" + (change)="posthog.capture('API Keys: api key title is edited')" [(ngModel)]="generatingAPIkeyTitle"> diff --git a/frontend/src/app/components/audit/audit.component.ts b/frontend/src/app/components/audit/audit.component.ts index 4bfb7ad20..2969e8f5a 100644 --- a/frontend/src/app/components/audit/audit.component.ts +++ b/frontend/src/app/components/audit/audit.component.ts @@ -2,9 +2,9 @@ import { AsyncPipe, NgClass, NgForOf, NgIf } from '@angular/common'; import { Component, OnInit, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; @@ -12,6 +12,7 @@ import { Title } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { User } from '@sentry/angular'; import { Angulartics2OnModule } from 'angulartics2'; +import posthog from 'posthog-js'; import { merge } from 'rxjs'; import { take, tap } from 'rxjs/operators'; import { normalizeTableName } from 'src/app/lib/normalize'; @@ -52,6 +53,7 @@ import { InfoDialogComponent } from './info-dialog/info-dialog.component'; styleUrls: ['./audit.component.css'], }) export class AuditComponent implements OnInit { + protected posthog = posthog; public isSaas = (environment as any).saas; public connectionID: string; public accesLevel: string; diff --git a/frontend/src/app/components/branding/branding.component.html b/frontend/src/app/components/branding/branding.component.html index b6fdf3181..4cce16f50 100644 --- a/frontend/src/app/components/branding/branding.component.html +++ b/frontend/src/app/components/branding/branding.component.html @@ -25,6 +25,7 @@

Branding

data-testid="custom-domain-input" angulartics2On="change" angularticsAction="Company: custom domain is edited" + (change)="posthog.capture('Company: custom domain is edited')" placeholder="e.g. {{companyCustomDomainPlaceholder}}" [readonly]="currentUser.role === 'DB_ADMIN' || isCustomDomain" [(ngModel)]="companyCustomDomainHostname"> @@ -95,6 +96,7 @@

Branding

data-testid="custom-tab-title-input" angulartics2On="change" angularticsAction="Company: tab title domain is edited" + (change)="posthog.capture('Company: tab title domain is edited')" placeholder="e.g. {{company.name}}" [readonly]="currentUser.role === 'DB_ADMIN'" [(ngModel)]="companyTabTitle"> diff --git a/frontend/src/app/components/branding/branding.component.ts b/frontend/src/app/components/branding/branding.component.ts index 7c095ccb6..e8d2ef665 100644 --- a/frontend/src/app/components/branding/branding.component.ts +++ b/frontend/src/app/components/branding/branding.component.ts @@ -1,3 +1,4 @@ +import { NgIf } from '@angular/common'; import { Component, CUSTOM_ELEMENTS_SCHEMA, OnDestroy, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; @@ -6,265 +7,288 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { NgIf } from '@angular/common'; +import { Title } from '@angular/platform-browser'; import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; +import posthog from 'posthog-js'; import { Subscription } from 'rxjs'; -import { Title } from '@angular/platform-browser'; - -import { AlertComponent } from '../ui-components/alert/alert.component'; +import { Company } from 'src/app/models/company'; import { CompanyService } from 'src/app/services/company.service'; import { UserService } from 'src/app/services/user.service'; -import { Company } from 'src/app/models/company'; -import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; -import { DeleteDomainDialogComponent } from '../company/delete-domain-dialog/delete-domain-dialog.component'; import { environment } from 'src/environments/environment'; +import { DeleteDomainDialogComponent } from '../company/delete-domain-dialog/delete-domain-dialog.component'; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; import { PlaceholderBrandingComponent } from '../skeletons/placeholder-branding/placeholder-branding.component'; +import { AlertComponent } from '../ui-components/alert/alert.component'; @Component({ - selector: 'app-branding', - templateUrl: './branding.component.html', - styleUrls: ['./branding.component.css'], - imports: [ - NgIf, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, - MatIconModule, - MatTooltipModule, - Angulartics2OnModule, - AlertComponent, - ProfileSidebarComponent, - PlaceholderBrandingComponent, - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + selector: 'app-branding', + templateUrl: './branding.component.html', + styleUrls: ['./branding.component.css'], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatTooltipModule, + Angulartics2OnModule, + AlertComponent, + ProfileSidebarComponent, + PlaceholderBrandingComponent, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class BrandingComponent implements OnInit, OnDestroy { - public isSaas = (environment as any).saas; - public company: Company = null; - public currentPlan: string; - public currentUser: any; - - public companyCustomDomain: { - id: string, - companyId: string, - hostname: string, - } = { - id: null, - companyId: '', - hostname: '', - }; - - public companyCustomDomainHostname: string; - public companyCustomDomainPlaceholder: string; - public companyCustomDomainThirdLevel: string; - public submittingCustomDomain: boolean = false; - public isCustomDomain: boolean = false; - - public submittingLogo: boolean = false; - public submittingFavicon: boolean = false; - - public companyTabTitle: string; - public submittingTabTitle: boolean = false; - - get whiteLabelSettings(): {logo: string, favicon: string, tabTitle: string} { - return this._company.whiteLabelSettings || { logo: '', favicon: '', tabTitle: '' }; - } - - get isDemo() { - return this._user.isDemo; - } - - private subscriptions: Subscription[] = []; - - constructor( - private _company: CompanyService, - private _user: UserService, - private dialog: MatDialog, - private angulartics2: Angulartics2, - private title: Title - ) {} - - ngOnInit() { - this.isCustomDomain = this._company.isCustomDomain() && this.isSaas; - - const titleSub = this._company.getCurrentTabTitle().subscribe(tabTitle => { - this.companyTabTitle = tabTitle; - this.title.setTitle(`Branding | ${tabTitle || 'Rocketadmin'}`); - }); - this.subscriptions.push(titleSub); - - const userSub = this._user.cast.subscribe(user => { - this.currentUser = user; - }); - this.subscriptions.push(userSub); - - this._company.fetchCompany().subscribe(res => { - this.company = res; - this.setCompanyPlan(res.subscriptionLevel); - if (this.isCustomDomain) { - this.companyCustomDomainHostname = res.custom_domain; - } else { - this.getCompanyCustomDomain(res.id); - } - }); - - const castSub = this._company.cast.subscribe(arg => { - if (arg === 'domain') { - this.getCompanyCustomDomain(this.company.id); - } else if (arg === 'updated-white-label-settings') { - this._company.getWhiteLabelProperties(this.company.id).subscribe(); - } - }); - this.subscriptions.push(castSub); - } - - ngOnDestroy() { - this.subscriptions.forEach(sub => sub.unsubscribe()); - } - - getCompanyCustomDomain(companyId: string) { - this._company.getCustomDomain(companyId).subscribe(res => { - if (res.success) { - this.companyCustomDomain = res.domain_info; - this.companyCustomDomainHostname = res.domain_info.hostname; - this.companyCustomDomainThirdLevel = this.companyCustomDomainHostname.split('.')[0]; - } else { - this.companyCustomDomain = { - id: null, - companyId: companyId, - hostname: '' - }; - this.companyCustomDomainHostname = ''; - this.companyCustomDomainPlaceholder = `${this.company.name.toLowerCase().replace(/[^a-z0-9]/g, '')}.example.com`; - this.companyCustomDomainThirdLevel = this.companyCustomDomainPlaceholder.split('.')[0]; - } - }); - } - - setCompanyPlan(subscriptionLevel: string) { - if (subscriptionLevel) { - this.currentPlan = subscriptionLevel.slice(0, -5).toLowerCase(); - } else { - this.currentPlan = 'free'; - } - } - - handleChangeCompanyDomain() { - this.submittingCustomDomain = true; - if (this.companyCustomDomain.id) { - this._company.updateCustomDomain(this.company.id, this.companyCustomDomain.id).subscribe(() => { - this.submittingCustomDomain = false; - this.angulartics2.eventTrack.next({ - action: 'Company: domain is updated successfully', - }); - }); - } else { - this._company.createCustomDomain(this.company.id, this.companyCustomDomainHostname).subscribe(() => { - this.submittingCustomDomain = false; - this.angulartics2.eventTrack.next({ - action: 'Company: domain is created successfully', - }); - }); - } - } - - handleDeleteDomainDialogOpen() { - this.dialog.open(DeleteDomainDialogComponent, { - width: '25em', - data: { companyId: this.company.id, domain: this.companyCustomDomainHostname } - }); - } - - onCompanyLogoSelected(event: any) { - this.submittingLogo = true; - - const input = event.target as HTMLInputElement; - const file = input.files?.[0]; - let companyLogoFile: File | null = null; - - if (file) { - companyLogoFile = file; - } else { - companyLogoFile = null; - } - - this._company.uploadLogo(this.company.id, companyLogoFile).subscribe(_res => { - this.submittingLogo = false; - this.angulartics2.eventTrack.next({ - action: 'Company: logo is uploaded successfully', - }); - }, _err => { - this.submittingLogo = false; - }); - } - - removeLogo() { - this.submittingLogo = true; - this._company.removeLogo(this.company.id).subscribe(_res => { - this.submittingLogo = false; - this.angulartics2.eventTrack.next({ - action: 'Company: logo is removed successfully', - }); - }, _err => { - this.submittingLogo = false; - }); - } - - onFaviconSelected(event: any) { - this.submittingFavicon = true; - - const input = event.target as HTMLInputElement; - const file = input.files?.[0]; - let faviconFile: File | null = null; - - if (file) { - faviconFile = file; - } else { - faviconFile = null; - } - - this._company.uploadFavicon(this.company.id, faviconFile).subscribe(_res => { - this.submittingFavicon = false; - this.angulartics2.eventTrack.next({ - action: 'Company: favicon is uploaded successfully', - }); - }, _err => { - this.submittingFavicon = false; - }); - } - - removeFavicon() { - this.submittingFavicon = true; - this._company.removeFavicon(this.company.id).subscribe(_res => { - this.submittingFavicon = false; - this.angulartics2.eventTrack.next({ - action: 'Company: favicon is removed successfully', - }); - }, _err => { - this.submittingFavicon = false; - }); - } - - updateTabTitle() { - this.submittingTabTitle = true; - this._company.updateTabTitle(this.company.id, this.companyTabTitle).subscribe(() => { - this.submittingTabTitle = false; - this.angulartics2.eventTrack.next({ - action: 'Company: tab title is updated successfully', - }); - }, _err => { - this.submittingTabTitle = false; - }); - } - - deleteTabTitle() { - this.submittingTabTitle = true; - this._company.removeTabTitle(this.company.id).subscribe(() => { - this.submittingTabTitle = false; - this.angulartics2.eventTrack.next({ - action: 'Company: tab title is deleted successfully', - }); - }); - } + protected posthog = posthog; + public isSaas = (environment as any).saas; + public company: Company = null; + public currentPlan: string; + public currentUser: any; + + public companyCustomDomain: { + id: string; + companyId: string; + hostname: string; + } = { + id: null, + companyId: '', + hostname: '', + }; + + public companyCustomDomainHostname: string; + public companyCustomDomainPlaceholder: string; + public companyCustomDomainThirdLevel: string; + public submittingCustomDomain: boolean = false; + public isCustomDomain: boolean = false; + + public submittingLogo: boolean = false; + public submittingFavicon: boolean = false; + + public companyTabTitle: string; + public submittingTabTitle: boolean = false; + + get whiteLabelSettings(): { logo: string; favicon: string; tabTitle: string } { + return this._company.whiteLabelSettings || { logo: '', favicon: '', tabTitle: '' }; + } + + get isDemo() { + return this._user.isDemo; + } + + private subscriptions: Subscription[] = []; + + constructor( + private _company: CompanyService, + private _user: UserService, + private dialog: MatDialog, + private angulartics2: Angulartics2, + private title: Title, + ) {} + + ngOnInit() { + this.isCustomDomain = this._company.isCustomDomain() && this.isSaas; + + const titleSub = this._company.getCurrentTabTitle().subscribe((tabTitle) => { + this.companyTabTitle = tabTitle; + this.title.setTitle(`Branding | ${tabTitle || 'Rocketadmin'}`); + }); + this.subscriptions.push(titleSub); + + const userSub = this._user.cast.subscribe((user) => { + this.currentUser = user; + }); + this.subscriptions.push(userSub); + + this._company.fetchCompany().subscribe((res) => { + this.company = res; + this.setCompanyPlan(res.subscriptionLevel); + if (this.isCustomDomain) { + this.companyCustomDomainHostname = res.custom_domain; + } else { + this.getCompanyCustomDomain(res.id); + } + }); + + const castSub = this._company.cast.subscribe((arg) => { + if (arg === 'domain') { + this.getCompanyCustomDomain(this.company.id); + } else if (arg === 'updated-white-label-settings') { + this._company.getWhiteLabelProperties(this.company.id).subscribe(); + } + }); + this.subscriptions.push(castSub); + } + + ngOnDestroy() { + this.subscriptions.forEach((sub) => sub.unsubscribe()); + } + + getCompanyCustomDomain(companyId: string) { + this._company.getCustomDomain(companyId).subscribe((res) => { + if (res.success) { + this.companyCustomDomain = res.domain_info; + this.companyCustomDomainHostname = res.domain_info.hostname; + this.companyCustomDomainThirdLevel = this.companyCustomDomainHostname.split('.')[0]; + } else { + this.companyCustomDomain = { + id: null, + companyId: companyId, + hostname: '', + }; + this.companyCustomDomainHostname = ''; + this.companyCustomDomainPlaceholder = `${this.company.name.toLowerCase().replace(/[^a-z0-9]/g, '')}.example.com`; + this.companyCustomDomainThirdLevel = this.companyCustomDomainPlaceholder.split('.')[0]; + } + }); + } + + setCompanyPlan(subscriptionLevel: string) { + if (subscriptionLevel) { + this.currentPlan = subscriptionLevel.slice(0, -5).toLowerCase(); + } else { + this.currentPlan = 'free'; + } + } + + handleChangeCompanyDomain() { + this.submittingCustomDomain = true; + if (this.companyCustomDomain.id) { + this._company.updateCustomDomain(this.company.id, this.companyCustomDomain.id).subscribe(() => { + this.submittingCustomDomain = false; + this.angulartics2.eventTrack.next({ + action: 'Company: domain is updated successfully', + }); + posthog.capture('Company: domain is updated successfully'); + }); + } else { + this._company.createCustomDomain(this.company.id, this.companyCustomDomainHostname).subscribe(() => { + this.submittingCustomDomain = false; + this.angulartics2.eventTrack.next({ + action: 'Company: domain is created successfully', + }); + posthog.capture('Company: domain is created successfully'); + }); + } + } + + handleDeleteDomainDialogOpen() { + this.dialog.open(DeleteDomainDialogComponent, { + width: '25em', + data: { companyId: this.company.id, domain: this.companyCustomDomainHostname }, + }); + } + + onCompanyLogoSelected(event: any) { + this.submittingLogo = true; + + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + let companyLogoFile: File | null = null; + + if (file) { + companyLogoFile = file; + } else { + companyLogoFile = null; + } + + this._company.uploadLogo(this.company.id, companyLogoFile).subscribe( + (_res) => { + this.submittingLogo = false; + this.angulartics2.eventTrack.next({ + action: 'Company: logo is uploaded successfully', + }); + posthog.capture('Company: logo is uploaded successfully'); + }, + (_err) => { + this.submittingLogo = false; + }, + ); + } + + removeLogo() { + this.submittingLogo = true; + this._company.removeLogo(this.company.id).subscribe( + (_res) => { + this.submittingLogo = false; + this.angulartics2.eventTrack.next({ + action: 'Company: logo is removed successfully', + }); + posthog.capture('Company: logo is removed successfully'); + }, + (_err) => { + this.submittingLogo = false; + }, + ); + } + + onFaviconSelected(event: any) { + this.submittingFavicon = true; + + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + let faviconFile: File | null = null; + + if (file) { + faviconFile = file; + } else { + faviconFile = null; + } + + this._company.uploadFavicon(this.company.id, faviconFile).subscribe( + (_res) => { + this.submittingFavicon = false; + this.angulartics2.eventTrack.next({ + action: 'Company: favicon is uploaded successfully', + }); + posthog.capture('Company: favicon is uploaded successfully'); + }, + (_err) => { + this.submittingFavicon = false; + }, + ); + } + + removeFavicon() { + this.submittingFavicon = true; + this._company.removeFavicon(this.company.id).subscribe( + (_res) => { + this.submittingFavicon = false; + this.angulartics2.eventTrack.next({ + action: 'Company: favicon is removed successfully', + }); + posthog.capture('Company: favicon is removed successfully'); + }, + (_err) => { + this.submittingFavicon = false; + }, + ); + } + + updateTabTitle() { + this.submittingTabTitle = true; + this._company.updateTabTitle(this.company.id, this.companyTabTitle).subscribe( + () => { + this.submittingTabTitle = false; + this.angulartics2.eventTrack.next({ + action: 'Company: tab title is updated successfully', + }); + posthog.capture('Company: tab title is updated successfully'); + }, + (_err) => { + this.submittingTabTitle = false; + }, + ); + } + + deleteTabTitle() { + this.submittingTabTitle = true; + this._company.removeTabTitle(this.company.id).subscribe(() => { + this.submittingTabTitle = false; + this.angulartics2.eventTrack.next({ + action: 'Company: tab title is deleted successfully', + }); + posthog.capture('Company: tab title is deleted successfully'); + }); + } } diff --git a/frontend/src/app/components/charts/chart-delete-dialog/chart-delete-dialog.component.ts b/frontend/src/app/components/charts/chart-delete-dialog/chart-delete-dialog.component.ts index 40f2fdc51..98a5c4899 100644 --- a/frontend/src/app/components/charts/chart-delete-dialog/chart-delete-dialog.component.ts +++ b/frontend/src/app/components/charts/chart-delete-dialog/chart-delete-dialog.component.ts @@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { SavedQuery } from 'src/app/models/saved-query'; import { SavedQueriesService } from 'src/app/services/saved-queries.service'; @@ -30,6 +31,7 @@ export class ChartDeleteDialogComponent { this.angulartics2.eventTrack.next({ action: 'Charts: saved query deleted successfully', }); + posthog.capture('Charts: saved query deleted successfully'); this.submitting.set(false); this.dialogRef.close(true); }, diff --git a/frontend/src/app/components/charts/chart-edit/chart-edit.component.ts b/frontend/src/app/components/charts/chart-edit/chart-edit.component.ts index 6cf393fbc..a6d5538ae 100644 --- a/frontend/src/app/components/charts/chart-edit/chart-edit.component.ts +++ b/frontend/src/app/components/charts/chart-edit/chart-edit.component.ts @@ -14,6 +14,7 @@ import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { CodeEditorModule } from '@ngstack/code-editor'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { finalize } from 'rxjs/operators'; import { ChartType, TestQueryResult } from 'src/app/models/saved-query'; import { ConnectionsService } from 'src/app/services/connections.service'; @@ -206,6 +207,7 @@ export class ChartEditComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Charts: test query executed', }); + posthog.capture('Charts: test query executed'); } saveQuery(): void { @@ -246,6 +248,7 @@ export class ChartEditComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Charts: saved query updated', }); + posthog.capture('Charts: saved query updated'); } else { this._savedQueries .createSavedQuery(this.connectionId(), payload) @@ -256,6 +259,7 @@ export class ChartEditComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Charts: saved query created', }); + posthog.capture('Charts: saved query created'); } } diff --git a/frontend/src/app/components/charts/charts-list/charts-list.component.ts b/frontend/src/app/components/charts/charts-list/charts-list.component.ts index ce79515eb..f211fcaa3 100644 --- a/frontend/src/app/components/charts/charts-list/charts-list.component.ts +++ b/frontend/src/app/components/charts/charts-list/charts-list.component.ts @@ -4,25 +4,26 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; -import { MatTableModule } from '@angular/material/table'; import { MatDialog } from '@angular/material/dialog'; import { MatDividerModule } from '@angular/material/divider'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; +import { MatTableModule } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { ChartType, SavedQuery } from 'src/app/models/saved-query'; import { ConnectionsService } from 'src/app/services/connections.service'; import { SavedQueriesService } from 'src/app/services/saved-queries.service'; import { ChartMiniPreviewComponent } from '../../dashboards/chart-mini-preview/chart-mini-preview.component'; +import { DashboardsSidebarComponent } from '../../dashboards/dashboards-sidebar/dashboards-sidebar.component'; import { PlaceholderTableDataComponent } from '../../skeletons/placeholder-table-data/placeholder-table-data.component'; import { AlertComponent } from '../../ui-components/alert/alert.component'; import { ChartDeleteDialogComponent } from '../chart-delete-dialog/chart-delete-dialog.component'; -import { DashboardsSidebarComponent } from '../../dashboards/dashboards-sidebar/dashboards-sidebar.component'; @Component({ selector: 'app-charts-list', @@ -123,12 +124,14 @@ export class ChartsListComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Charts: create chart page opened', }); + posthog.capture('Charts: create chart page opened'); } trackEditPageOpened(): void { this.angulartics2.eventTrack.next({ action: 'Charts: edit chart page opened', }); + posthog.capture('Charts: edit chart page opened'); } openQuery(query: SavedQuery): void { @@ -144,6 +147,7 @@ export class ChartsListComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Charts: delete chart dialog opened', }); + posthog.capture('Charts: delete chart dialog opened'); } formatUpdatedAt(date: string): string { diff --git a/frontend/src/app/components/company-member-invitation/company-member-invitation.component.ts b/frontend/src/app/components/company-member-invitation/company-member-invitation.component.ts index 959cdb93e..04c6ba7db 100644 --- a/frontend/src/app/components/company-member-invitation/company-member-invitation.component.ts +++ b/frontend/src/app/components/company-member-invitation/company-member-invitation.component.ts @@ -1,81 +1,82 @@ -import { ActivatedRoute, ParamMap, Router } from '@angular/router'; -import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MatDividerModule } from '@angular/material/divider'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { MatDividerModule } from '@angular/material/divider'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { map } from 'rxjs/operators'; import { AuthService } from 'src/app/services/auth.service'; import { CompanyService } from 'src/app/services/company.service'; -import { Angulartics2 } from 'angulartics2'; import { PlaceholderCompanyInvitationComponent } from '../skeletons/placeholder-company-invitation/placeholder-company-invitation.component'; -import { BannerComponent } from '../ui-components/banner/banner.component'; import { AlertComponent } from '../ui-components/alert/alert.component'; +import { BannerComponent } from '../ui-components/banner/banner.component'; import { UserPasswordComponent } from '../ui-components/user-password/user-password.component'; @Component({ - selector: 'app-company-member-invitation', - templateUrl: './company-member-invitation.component.html', - styleUrls: ['./company-member-invitation.component.css'], - imports: [ - CommonModule, - FormsModule, - MatButtonModule, - MatFormFieldModule, - MatInputModule, - MatDividerModule, - PlaceholderCompanyInvitationComponent, - BannerComponent, - AlertComponent, - UserPasswordComponent - ] + selector: 'app-company-member-invitation', + templateUrl: './company-member-invitation.component.html', + styleUrls: ['./company-member-invitation.component.css'], + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + MatDividerModule, + PlaceholderCompanyInvitationComponent, + BannerComponent, + AlertComponent, + UserPasswordComponent, + ], }) export class CompanyMemberInvitationComponent implements OnInit { + public token: string; + public companyName: string = null; + public password: string = ''; + public userName: string; + public submitting: boolean; + public checkingLink: boolean = true; + public isLinkAvailable: boolean = false; - public token: string; - public companyName: string = null; - public password: string = ''; - public userName: string; - public submitting: boolean; - public checkingLink: boolean = true; - public isLinkAvailable: boolean = false; - - constructor( - private _auth: AuthService, - private _company: CompanyService, - private router: Router, - private route: ActivatedRoute, - private angulartics2: Angulartics2, - ) {} - - ngOnInit(): void { - this.route.paramMap - .pipe( - map((params: ParamMap) => { - this.token = params.get('verification-token'); - const companyId = params.get('company-id'); - this._company.fetchCompanyName(companyId).subscribe(res => this.companyName = res.name); - this._auth.checkInvitationAvailability(this.token).subscribe(res => { - this.isLinkAvailable = res.success; - this.checkingLink = false; - }) - }) - ).subscribe(); - } + constructor( + private _auth: AuthService, + private _company: CompanyService, + private router: Router, + private route: ActivatedRoute, + private angulartics2: Angulartics2, + ) {} - updatePasswordField(updatedValue: string) { - this.password = updatedValue; - } + ngOnInit(): void { + this.route.paramMap + .pipe( + map((params: ParamMap) => { + this.token = params.get('verification-token'); + const companyId = params.get('company-id'); + this._company.fetchCompanyName(companyId).subscribe((res) => (this.companyName = res.name)); + this._auth.checkInvitationAvailability(this.token).subscribe((res) => { + this.isLinkAvailable = res.success; + this.checkingLink = false; + }); + }), + ) + .subscribe(); + } - acceptInvitation() { - this._auth.acceptCompanyInvitation(this.token, this.password, this.userName).subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Company invitation: invitation is accepted successfully', - }); - this.router.navigate(['/connections-list']) - }) - } + updatePasswordField(updatedValue: string) { + this.password = updatedValue; + } + acceptInvitation() { + this._auth.acceptCompanyInvitation(this.token, this.password, this.userName).subscribe(() => { + this.angulartics2.eventTrack.next({ + action: 'Company invitation: invitation is accepted successfully', + }); + posthog.capture('Company invitation: invitation is accepted successfully'); + this.router.navigate(['/connections-list']); + }); + } } diff --git a/frontend/src/app/components/company/company.component.html b/frontend/src/app/components/company/company.component.html index b9d84b6e8..8940685a8 100644 --- a/frontend/src/app/components/company/company.component.html +++ b/frontend/src/app/components/company/company.component.html @@ -21,6 +21,7 @@

data-testid="company-name-input" angulartics2On="change" angularticsAction="Company: company name is edited" + (change)="posthog.capture('Company: company name is edited')" [(ngModel)]="company.name">
@@ -44,7 +45,8 @@

[href]="company.portal_link" target="_blank" data-testid="company-payment-portal-link" angulartics2On="click" - angularticsAction="Company: Payment settings is clicked"> + angularticsAction="Company: Payment settings is clicked" + (click)="posthog.capture('Company: Payment settings is clicked')"> Payment settings Stripe @@ -53,7 +55,8 @@

routerLink="/upgrade" data-testid="company-payment-upgrade-link" angulartics2On="click" - angularticsAction="Company: Upgrade is clicked"> + angularticsAction="Company: Upgrade is clicked" + (click)="posthog.capture('Company: Upgrade is clicked')"> {{ company.portal_link ? 'Change' : 'Upgrade'}}

@@ -69,7 +72,7 @@

Members Invite member @@ -125,7 +128,8 @@

Members + angularticsAction="Company: role is clicked" + (click)="posthog.capture('Company: role is clicked')"> {{ companyRolesName[element.role] }} arrow_drop_down @@ -163,6 +167,7 @@

Members @@ -215,6 +220,7 @@

Members @@ -243,7 +249,7 @@

Members + (click)="handleRevokeInvitationDialogOpen(element.email); posthog.capture('Company: Revoke invitation is clicked')"> Revoke @@ -252,7 +258,7 @@

Members + (click)="handleDeleteMemberDialogOpen(element); posthog.capture('Company: Delete member is clicked')"> Delete diff --git a/frontend/src/app/components/company/company.component.ts b/frontend/src/app/components/company/company.component.ts index 2e851bdbe..676df1aad 100644 --- a/frontend/src/app/components/company/company.component.ts +++ b/frontend/src/app/components/company/company.component.ts @@ -1,13 +1,6 @@ -import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; -import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core'; -import { Company, CompanyMember, CompanyMemberRole } from 'src/app/models/company'; - -import { AlertComponent } from '../ui-components/alert/alert.component'; -import { CompanyService } from 'src/app/services/company.service'; -import { DeleteDomainDialogComponent } from './delete-domain-dialog/delete-domain-dialog.component'; -import { DeleteMemberDialogComponent } from './delete-member-dialog/delete-member-dialog.component'; +import { NgIf } from '@angular/common'; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { InviteMemberDialogComponent } from './invite-member-dialog/invite-member-dialog.component'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -18,412 +11,447 @@ import { MatSelectModule } from '@angular/material/select'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatTableModule } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { NgIf } from '@angular/common'; -import { PlaceholderCompanyComponent } from '../skeletons/placeholder-company/placeholder-company.component'; -import { PlaceholderTableDataComponent } from '../skeletons/placeholder-table-data/placeholder-table-data.component'; -import { RevokeInvitationDialogComponent } from './revoke-invitation-dialog/revoke-invitation-dialog.component'; +import { Title } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; +import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; +import { orderBy } from 'lodash-es'; +import posthog from 'posthog-js'; import { Subscription } from 'rxjs'; +import { Company, CompanyMember, CompanyMemberRole } from 'src/app/models/company'; import { SubscriptionPlans } from 'src/app/models/user'; -import { Title } from '@angular/platform-browser'; +import { CompanyService } from 'src/app/services/company.service'; import { UserService } from 'src/app/services/user.service'; import { environment } from 'src/environments/environment'; -import { orderBy } from "lodash-es"; import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; +import { PlaceholderCompanyComponent } from '../skeletons/placeholder-company/placeholder-company.component'; +import { PlaceholderTableDataComponent } from '../skeletons/placeholder-table-data/placeholder-table-data.component'; +import { AlertComponent } from '../ui-components/alert/alert.component'; +import { DeleteDomainDialogComponent } from './delete-domain-dialog/delete-domain-dialog.component'; +import { DeleteMemberDialogComponent } from './delete-member-dialog/delete-member-dialog.component'; +import { InviteMemberDialogComponent } from './invite-member-dialog/invite-member-dialog.component'; +import { RevokeInvitationDialogComponent } from './revoke-invitation-dialog/revoke-invitation-dialog.component'; @Component({ - selector: 'app-company', - templateUrl: './company.component.html', - styleUrls: ['./company.component.css'], - imports: [ - NgIf, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, - MatIconModule, - MatSelectModule, - MatSlideToggleModule, - MatTooltipModule, - MatTableModule, - MatMenuModule, - RouterModule, - Angulartics2OnModule, - AlertComponent, - PlaceholderCompanyComponent, - PlaceholderTableDataComponent, - ProfileSidebarComponent - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + selector: 'app-company', + templateUrl: './company.component.html', + styleUrls: ['./company.component.css'], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatSelectModule, + MatSlideToggleModule, + MatTooltipModule, + MatTableModule, + MatMenuModule, + RouterModule, + Angulartics2OnModule, + AlertComponent, + PlaceholderCompanyComponent, + PlaceholderTableDataComponent, + ProfileSidebarComponent, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class CompanyComponent { - CompanyMemberRole = CompanyMemberRole; - public isSaas = (environment as any).saas; - public company: Company = null; - public members: any = null; - public currentPlan: string; - public submitting: boolean; - public usersCount: number; - public adminsCount: number; - public unsuspendedAdminsCount: number; - public membersTableDisplayedColumns: string[]; - // public invitationsTableDisplayColumns: string[] = ['invitedUserEmail', 'role']; - public submittingChangedName: boolean =false; - public currentUser; - public submittingUsersChange: boolean = false; - public companyCustomDomain: { - id: string, - companyId: string, - hostname: string, - } = { - id: null, - companyId: '', - hostname: '', - }; - - public companyCustomDomainHostname: string; - public companyCustomDomainPlaceholder: string; - public companyCustomDomainThirdLevel: string; - public submittingCustomDomain: boolean = false; - public isCustomDomain: boolean = false; - - public submittingLogo: boolean = false; - public submittingFavicon: boolean = false; - - public companyTabTitle: string; - public submittingTabTitle: boolean = false; - - public companyRolesName = { - 'ADMIN': 'Account Owner', - 'DB_ADMIN': 'System Admin', - 'USER': 'Member' - } - - public authProviderIcons = { - GOOGLE: 'google', - GITHUB: 'github', - } - - get whiteLabelSettings(): {logo: string, favicon: string, tabTitle: string} { - return this._company.whiteLabelSettings || { logo: '', favicon: '', tabTitle : '' }; - } - - get isDemo() { - return this._user.isDemo; - } - - private getTitleSubscription: Subscription; - - constructor( - public _company: CompanyService, - public _user: UserService, - public dialog: MatDialog, - private angulartics2: Angulartics2, - private title: Title - ) { } - - ngOnInit() { - this.isCustomDomain = this._company.isCustomDomain() && this.isSaas; - - this.getTitleSubscription = this._company.getCurrentTabTitle().subscribe(title => { - this.companyTabTitle = title; - this.title.setTitle(`Company settings | ${title || 'Rocketadmin'}`); - }); - - this._company.fetchCompany().subscribe(res => { - this.company = res; - this.setCompanyPlan(res.subscriptionLevel); - this.getCompanyMembers(res.id); - if (this.isCustomDomain) { - this.companyCustomDomainHostname = res.custom_domain; - } else { - this.getCompanyCustomDomain(res.id); - } - }); - - this._company.cast.subscribe( arg => { - if (arg === 'invited' || arg === 'revoked' || arg === 'role' || arg === 'suspended' || arg === 'unsuspended') { - this.submittingUsersChange = true; - this._company.fetchCompany().subscribe(res => { - this.company = res; - this.getCompanyMembers(res.id); - this.submittingUsersChange = false; - }); - } else if (arg === 'deleted') { - this.submittingUsersChange = true; - this.getCompanyMembers(this.company.id); - } else if (arg === 'domain') { - this.getCompanyCustomDomain(this.company.id); - } else if (arg === 'updated-white-label-settings') { - // this.submittingLogo = true; - this._company.getWhiteLabelProperties(this.company.id).subscribe(); - } - }); - } - - ngOnDestroy() { - if (this.getTitleSubscription) { - this.getTitleSubscription.unsubscribe(); - } - } - - getCompanyMembers(companyId: string) { - this._company.fetchCompanyMembers(companyId).subscribe(res => { - if (this.company.invitations) { - this.company.invitations.map( (invitee: any) => { - invitee.email = invitee.invitedUserEmail; - invitee.pending = true; - }) - } else { - this.company.invitations = []; - }; - this._user.cast - .subscribe(user => { - this.currentUser = res.find(member => member.email === user.email); - - if (this.currentUser.role === 'ADMIN') { - this.membersTableDisplayedColumns = ['email', 'name', 'role', 'twoFA', 'active', 'access', 'actions']; - } else { - this.membersTableDisplayedColumns = ['email', 'name', 'role', 'twoFA', 'access']; - } - - const currentMembers = orderBy(res, ['role', 'email']); - if (this.currentUser.role === 'ADMIN') { - const userIndex = currentMembers.findIndex(user => user.email === this.currentUser.email); - - if (userIndex !== -1) { - const user = currentMembers.splice(userIndex, 1)[0]; - currentMembers.unshift(user); - } - } - - this.members = [...currentMembers, ...this.company.invitations]; - }); - this.adminsCount = res.filter(user => user.role === 'ADMIN').length; - this.unsuspendedAdminsCount = res.filter(user => user.role === 'ADMIN' && !user.suspended).length; - this.usersCount = this.company.invitations.length + res.length; - this.submittingUsersChange = false; - }); - } - - getCompanyCustomDomain(companyId: string) { - this._company.getCustomDomain(companyId).subscribe(res => { - if (res.success) { - this.companyCustomDomain = res.domain_info; - this.companyCustomDomainHostname = res.domain_info.hostname; - this.companyCustomDomainThirdLevel = this.companyCustomDomainHostname.split('.')[0]; - } else { - this.companyCustomDomain = { - id: null, - companyId: companyId, - hostname: '' - }; - this.companyCustomDomainHostname = ''; - this.companyCustomDomainPlaceholder = `${this.company.name.toLowerCase().replace(/[^a-z0-9]/g, '')}.example.com`; - this.companyCustomDomainThirdLevel = this.companyCustomDomainPlaceholder.split('.')[0]; - } - }); - } - - setCompanyPlan(subscriptionLevel: SubscriptionPlans) { - if (subscriptionLevel) { - this.currentPlan = subscriptionLevel; - this.currentPlan = this.currentPlan.slice(0, -5).toLowerCase(); - } else { - this.currentPlan = "free" - } - } - - handleAddMemberDialogOpen() { - this.dialog.open(InviteMemberDialogComponent, { - width: '25em', - data: this.company - }); - } - - handleDeleteMemberDialogOpen(user: CompanyMember) { - this.dialog.open(DeleteMemberDialogComponent, { - width: '25em', - data: {companyId: this.company.id, user} - }); - } - - handleRevokeInvitationDialogOpen(userEmail: string) { - this.dialog.open(RevokeInvitationDialogComponent, { - width: '25em', - data: {companyId: this.company.id, userEmail} - }); - } - - changeCompanyName() { - this.submittingChangedName = true; - this._company.updateCompanyName(this.company.id, this.company.name).subscribe( - () => { - this.angulartics2.eventTrack.next({ - action: 'Company: company name is updated successfully', - }); - this.submittingChangedName = false - }, - () => this.submittingChangedName = false, - () => this.submittingChangedName = false - ); - } - - updateRole(userId: string, userRole: CompanyMemberRole) { - this.submittingUsersChange = true; - this._company.updateCompanyMemberRole(this.company.id, userId, userRole).subscribe(() => { - this.getCompanyMembers(this.company.id); - this.angulartics2.eventTrack.next({ - action: 'Company: role is updated successfully', - }); - }); - } - - switchSuspendance(isSuspendance: boolean, memberEmail: string) { - console.log(isSuspendance, memberEmail); - this.submittingUsersChange = true; - if (isSuspendance) { - this._company.suspendCompanyMember(this.company.id, [memberEmail]).subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Company: member is suspended', - }); - }); - } else { - this._company.restoreCompanyMember(this.company.id, [memberEmail]).subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Company: member is restored', - }); - }); - } - } - - changeShowTestConnections(checked: boolean) { - const displayMode = checked ? 'on' : 'off'; - this.submitting = true; - this._company.updateShowTestConnections(displayMode).subscribe(() => { - this.submitting = false; - this.angulartics2.eventTrack.next({ - action: 'Company: show test connections is updated successfully', - }); - }); - } - - handleChangeCompanyDomain() { - this.submittingCustomDomain = true; - if (this.companyCustomDomain.id) { - this._company.updateCustomDomain(this.company.id, this.companyCustomDomain.id).subscribe(() => { - this.submittingCustomDomain = false; - this.angulartics2.eventTrack.next({ - action: 'Company: domain is updated successfully', - }); - }); - } else { - this._company.createCustomDomain(this.company.id, this.companyCustomDomainHostname).subscribe(() => { - this.submittingCustomDomain = false; - this.angulartics2.eventTrack.next({ - action: 'Company: domain is created successfully', - }); - }); - } - } - - handleDeleteDomainDialogOpen() { - this.dialog.open(DeleteDomainDialogComponent, { - width: '25em', - data: { companyId: this.company.id, domain: this.companyCustomDomainHostname } - }); - } - - onCompanyLogoSelected(event: any) { - this.submittingLogo = true; - - const input = event.target as HTMLInputElement; - const file = input.files?.[0]; - let companyLogoFile: File | null = null; - - if (file) { - companyLogoFile = file; - } else { - companyLogoFile = null; - } - - this._company.uploadLogo(this.company.id, companyLogoFile).subscribe(_res => { - this.submittingLogo = false; - this.angulartics2.eventTrack.next({ - action: 'Company: logo is uploaded successfully', - }); - }, _err => { - this.submittingLogo = false; - }); - } - - removeLogo() { - this.submittingLogo = true; - this._company.removeLogo(this.company.id).subscribe(_res => { - this.submittingLogo = false; - this.angulartics2.eventTrack.next({ - action: 'Company: logo is removed successfully', - }); - }, _err => { - this.submittingLogo = false; - }); - } - - onFaviconSelected(event: any) { - console.log('favicon selected'); - this.submittingFavicon = true; - - const input = event.target as HTMLInputElement; - const file = input.files?.[0]; - let faviconFile: File | null = null; - - if (file) { - faviconFile = file; - } else { - faviconFile = null; - } - - this._company.uploadFavicon(this.company.id, faviconFile).subscribe(_res => { - this.submittingFavicon = false; - this.angulartics2.eventTrack.next({ - action: 'Company: favicon is uploaded successfully', - }); - }, _err => { - this.submittingFavicon = false; - }); - } - - removeFavicon() { - this.submittingFavicon = true; - this._company.removeFavicon(this.company.id).subscribe(_res => { - this.submittingFavicon = false; - this.angulartics2.eventTrack.next({ - action: 'Company: favicon is removed successfully', - }); - }, _err => { - this.submittingFavicon = false; - }); - } - - updateTabTitle() { - this.submittingTabTitle = true; - this._company.updateTabTitle(this.company.id, this.companyTabTitle).subscribe(() => { - this.submittingTabTitle = false; - this.angulartics2.eventTrack.next({ - action: 'Company: tab title is updated successfully', - }); - }, _err => { - this.submittingTabTitle = false; - }); - } - - deleteTabTitle() { - this.submittingTabTitle = true; - this._company.removeTabTitle(this.company.id).subscribe(() => { - this.submittingTabTitle = false; - this.angulartics2.eventTrack.next({ - action: 'Company: tab title is deleted successfully', - }); - }); - } + protected posthog = posthog; + CompanyMemberRole = CompanyMemberRole; + public isSaas = (environment as any).saas; + public company: Company = null; + public members: any = null; + public currentPlan: string; + public submitting: boolean; + public usersCount: number; + public adminsCount: number; + public unsuspendedAdminsCount: number; + public membersTableDisplayedColumns: string[]; + // public invitationsTableDisplayColumns: string[] = ['invitedUserEmail', 'role']; + public submittingChangedName: boolean = false; + public currentUser; + public submittingUsersChange: boolean = false; + public companyCustomDomain: { + id: string; + companyId: string; + hostname: string; + } = { + id: null, + companyId: '', + hostname: '', + }; + + public companyCustomDomainHostname: string; + public companyCustomDomainPlaceholder: string; + public companyCustomDomainThirdLevel: string; + public submittingCustomDomain: boolean = false; + public isCustomDomain: boolean = false; + + public submittingLogo: boolean = false; + public submittingFavicon: boolean = false; + + public companyTabTitle: string; + public submittingTabTitle: boolean = false; + + public companyRolesName = { + ADMIN: 'Account Owner', + DB_ADMIN: 'System Admin', + USER: 'Member', + }; + + public authProviderIcons = { + GOOGLE: 'google', + GITHUB: 'github', + }; + + get whiteLabelSettings(): { logo: string; favicon: string; tabTitle: string } { + return this._company.whiteLabelSettings || { logo: '', favicon: '', tabTitle: '' }; + } + + get isDemo() { + return this._user.isDemo; + } + + private getTitleSubscription: Subscription; + + constructor( + public _company: CompanyService, + public _user: UserService, + public dialog: MatDialog, + private angulartics2: Angulartics2, + private title: Title, + ) {} + + ngOnInit() { + this.isCustomDomain = this._company.isCustomDomain() && this.isSaas; + + this.getTitleSubscription = this._company.getCurrentTabTitle().subscribe((title) => { + this.companyTabTitle = title; + this.title.setTitle(`Company settings | ${title || 'Rocketadmin'}`); + }); + + this._company.fetchCompany().subscribe((res) => { + this.company = res; + this.setCompanyPlan(res.subscriptionLevel); + this.getCompanyMembers(res.id); + if (this.isCustomDomain) { + this.companyCustomDomainHostname = res.custom_domain; + } else { + this.getCompanyCustomDomain(res.id); + } + }); + + this._company.cast.subscribe((arg) => { + if (arg === 'invited' || arg === 'revoked' || arg === 'role' || arg === 'suspended' || arg === 'unsuspended') { + this.submittingUsersChange = true; + this._company.fetchCompany().subscribe((res) => { + this.company = res; + this.getCompanyMembers(res.id); + this.submittingUsersChange = false; + }); + } else if (arg === 'deleted') { + this.submittingUsersChange = true; + this.getCompanyMembers(this.company.id); + } else if (arg === 'domain') { + this.getCompanyCustomDomain(this.company.id); + } else if (arg === 'updated-white-label-settings') { + // this.submittingLogo = true; + this._company.getWhiteLabelProperties(this.company.id).subscribe(); + } + }); + } + + ngOnDestroy() { + if (this.getTitleSubscription) { + this.getTitleSubscription.unsubscribe(); + } + } + + getCompanyMembers(companyId: string) { + this._company.fetchCompanyMembers(companyId).subscribe((res) => { + if (this.company.invitations) { + this.company.invitations.map((invitee: any) => { + invitee.email = invitee.invitedUserEmail; + invitee.pending = true; + }); + } else { + this.company.invitations = []; + } + this._user.cast.subscribe((user) => { + this.currentUser = res.find((member) => member.email === user.email); + + if (this.currentUser.role === 'ADMIN') { + this.membersTableDisplayedColumns = ['email', 'name', 'role', 'twoFA', 'active', 'access', 'actions']; + } else { + this.membersTableDisplayedColumns = ['email', 'name', 'role', 'twoFA', 'access']; + } + + const currentMembers = orderBy(res, ['role', 'email']); + if (this.currentUser.role === 'ADMIN') { + const userIndex = currentMembers.findIndex((user) => user.email === this.currentUser.email); + + if (userIndex !== -1) { + const user = currentMembers.splice(userIndex, 1)[0]; + currentMembers.unshift(user); + } + } + + this.members = [...currentMembers, ...this.company.invitations]; + }); + this.adminsCount = res.filter((user) => user.role === 'ADMIN').length; + this.unsuspendedAdminsCount = res.filter((user) => user.role === 'ADMIN' && !user.suspended).length; + this.usersCount = this.company.invitations.length + res.length; + this.submittingUsersChange = false; + }); + } + + getCompanyCustomDomain(companyId: string) { + this._company.getCustomDomain(companyId).subscribe((res) => { + if (res.success) { + this.companyCustomDomain = res.domain_info; + this.companyCustomDomainHostname = res.domain_info.hostname; + this.companyCustomDomainThirdLevel = this.companyCustomDomainHostname.split('.')[0]; + } else { + this.companyCustomDomain = { + id: null, + companyId: companyId, + hostname: '', + }; + this.companyCustomDomainHostname = ''; + this.companyCustomDomainPlaceholder = `${this.company.name.toLowerCase().replace(/[^a-z0-9]/g, '')}.example.com`; + this.companyCustomDomainThirdLevel = this.companyCustomDomainPlaceholder.split('.')[0]; + } + }); + } + + setCompanyPlan(subscriptionLevel: SubscriptionPlans) { + if (subscriptionLevel) { + this.currentPlan = subscriptionLevel; + this.currentPlan = this.currentPlan.slice(0, -5).toLowerCase(); + } else { + this.currentPlan = 'free'; + } + } + + handleAddMemberDialogOpen() { + this.dialog.open(InviteMemberDialogComponent, { + width: '25em', + data: this.company, + }); + } + + handleDeleteMemberDialogOpen(user: CompanyMember) { + this.dialog.open(DeleteMemberDialogComponent, { + width: '25em', + data: { companyId: this.company.id, user }, + }); + } + + handleRevokeInvitationDialogOpen(userEmail: string) { + this.dialog.open(RevokeInvitationDialogComponent, { + width: '25em', + data: { companyId: this.company.id, userEmail }, + }); + } + + changeCompanyName() { + this.submittingChangedName = true; + this._company.updateCompanyName(this.company.id, this.company.name).subscribe( + () => { + this.angulartics2.eventTrack.next({ + action: 'Company: company name is updated successfully', + }); + posthog.capture('Company: company name is updated successfully'); + this.submittingChangedName = false; + }, + () => (this.submittingChangedName = false), + () => (this.submittingChangedName = false), + ); + } + + updateRole(userId: string, userRole: CompanyMemberRole) { + this.submittingUsersChange = true; + this._company.updateCompanyMemberRole(this.company.id, userId, userRole).subscribe(() => { + this.getCompanyMembers(this.company.id); + this.angulartics2.eventTrack.next({ + action: 'Company: role is updated successfully', + }); + posthog.capture('Company: role is updated successfully'); + }); + } + + switchSuspendance(isSuspendance: boolean, memberEmail: string) { + console.log(isSuspendance, memberEmail); + this.submittingUsersChange = true; + if (isSuspendance) { + this._company.suspendCompanyMember(this.company.id, [memberEmail]).subscribe(() => { + this.angulartics2.eventTrack.next({ + action: 'Company: member is suspended', + }); + posthog.capture('Company: member is suspended'); + }); + } else { + this._company.restoreCompanyMember(this.company.id, [memberEmail]).subscribe(() => { + this.angulartics2.eventTrack.next({ + action: 'Company: member is restored', + }); + posthog.capture('Company: member is restored'); + }); + } + } + + changeShowTestConnections(checked: boolean) { + const displayMode = checked ? 'on' : 'off'; + this.submitting = true; + this._company.updateShowTestConnections(displayMode).subscribe(() => { + this.submitting = false; + this.angulartics2.eventTrack.next({ + action: 'Company: show test connections is updated successfully', + }); + posthog.capture('Company: show test connections is updated successfully'); + }); + } + + handleChangeCompanyDomain() { + this.submittingCustomDomain = true; + if (this.companyCustomDomain.id) { + this._company.updateCustomDomain(this.company.id, this.companyCustomDomain.id).subscribe(() => { + this.submittingCustomDomain = false; + this.angulartics2.eventTrack.next({ + action: 'Company: domain is updated successfully', + }); + posthog.capture('Company: domain is updated successfully'); + }); + } else { + this._company.createCustomDomain(this.company.id, this.companyCustomDomainHostname).subscribe(() => { + this.submittingCustomDomain = false; + this.angulartics2.eventTrack.next({ + action: 'Company: domain is created successfully', + }); + posthog.capture('Company: domain is created successfully'); + }); + } + } + + handleDeleteDomainDialogOpen() { + this.dialog.open(DeleteDomainDialogComponent, { + width: '25em', + data: { companyId: this.company.id, domain: this.companyCustomDomainHostname }, + }); + } + + onCompanyLogoSelected(event: any) { + this.submittingLogo = true; + + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + let companyLogoFile: File | null = null; + + if (file) { + companyLogoFile = file; + } else { + companyLogoFile = null; + } + + this._company.uploadLogo(this.company.id, companyLogoFile).subscribe( + (_res) => { + this.submittingLogo = false; + this.angulartics2.eventTrack.next({ + action: 'Company: logo is uploaded successfully', + }); + posthog.capture('Company: logo is uploaded successfully'); + }, + (_err) => { + this.submittingLogo = false; + }, + ); + } + + removeLogo() { + this.submittingLogo = true; + this._company.removeLogo(this.company.id).subscribe( + (_res) => { + this.submittingLogo = false; + this.angulartics2.eventTrack.next({ + action: 'Company: logo is removed successfully', + }); + posthog.capture('Company: logo is removed successfully'); + }, + (_err) => { + this.submittingLogo = false; + }, + ); + } + + onFaviconSelected(event: any) { + console.log('favicon selected'); + this.submittingFavicon = true; + + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + let faviconFile: File | null = null; + + if (file) { + faviconFile = file; + } else { + faviconFile = null; + } + + this._company.uploadFavicon(this.company.id, faviconFile).subscribe( + (_res) => { + this.submittingFavicon = false; + this.angulartics2.eventTrack.next({ + action: 'Company: favicon is uploaded successfully', + }); + posthog.capture('Company: favicon is uploaded successfully'); + }, + (_err) => { + this.submittingFavicon = false; + }, + ); + } + + removeFavicon() { + this.submittingFavicon = true; + this._company.removeFavicon(this.company.id).subscribe( + (_res) => { + this.submittingFavicon = false; + this.angulartics2.eventTrack.next({ + action: 'Company: favicon is removed successfully', + }); + posthog.capture('Company: favicon is removed successfully'); + }, + (_err) => { + this.submittingFavicon = false; + }, + ); + } + + updateTabTitle() { + this.submittingTabTitle = true; + this._company.updateTabTitle(this.company.id, this.companyTabTitle).subscribe( + () => { + this.submittingTabTitle = false; + this.angulartics2.eventTrack.next({ + action: 'Company: tab title is updated successfully', + }); + posthog.capture('Company: tab title is updated successfully'); + }, + (_err) => { + this.submittingTabTitle = false; + }, + ); + } + + deleteTabTitle() { + this.submittingTabTitle = true; + this._company.removeTabTitle(this.company.id).subscribe(() => { + this.submittingTabTitle = false; + this.angulartics2.eventTrack.next({ + action: 'Company: tab title is deleted successfully', + }); + posthog.capture('Company: tab title is deleted successfully'); + }); + } } diff --git a/frontend/src/app/components/company/delete-domain-dialog/delete-domain-dialog.component.ts b/frontend/src/app/components/company/delete-domain-dialog/delete-domain-dialog.component.ts index fdd8b05db..8aeb75419 100644 --- a/frontend/src/app/components/company/delete-domain-dialog/delete-domain-dialog.component.ts +++ b/frontend/src/app/components/company/delete-domain-dialog/delete-domain-dialog.component.ts @@ -3,39 +3,40 @@ import { Component, Inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { CompanyService } from 'src/app/services/company.service'; @Component({ - selector: 'app-delete-domain-dialog', - imports: [ - CommonModule, - MatDialogModule, - MatButtonModule, - ], - templateUrl: './delete-domain-dialog.component.html', - styleUrl: './delete-domain-dialog.component.css' + selector: 'app-delete-domain-dialog', + imports: [CommonModule, MatDialogModule, MatButtonModule], + templateUrl: './delete-domain-dialog.component.html', + styleUrl: './delete-domain-dialog.component.css', }) export class DeleteDomainDialogComponent { - public submitting: boolean = false; + public submitting: boolean = false; - constructor( - @Inject(MAT_DIALOG_DATA) public data: any, - public dialogRef: MatDialogRef, - public _company: CompanyService, - private angulartics2: Angulartics2, - ) { } + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + public dialogRef: MatDialogRef, + public _company: CompanyService, + private angulartics2: Angulartics2, + ) {} - deleteCompanyDomain() { - this.submitting = true; - this._company.deleteCustomDomain(this.data.companyId).subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Company: domain is deleted successfully', - }); - this.submitting = false; - this.dialogRef.close(); - }, - () => {}, - () => {this.submitting = false} - ); - } + deleteCompanyDomain() { + this.submitting = true; + this._company.deleteCustomDomain(this.data.companyId).subscribe( + () => { + this.angulartics2.eventTrack.next({ + action: 'Company: domain is deleted successfully', + }); + posthog.capture('Company: domain is deleted successfully'); + this.submitting = false; + this.dialogRef.close(); + }, + () => {}, + () => { + this.submitting = false; + }, + ); + } } diff --git a/frontend/src/app/components/company/delete-member-dialog/delete-member-dialog.component.ts b/frontend/src/app/components/company/delete-member-dialog/delete-member-dialog.component.ts index 2f7bdeb7b..7b8aa2607 100644 --- a/frontend/src/app/components/company/delete-member-dialog/delete-member-dialog.component.ts +++ b/frontend/src/app/components/company/delete-member-dialog/delete-member-dialog.component.ts @@ -1,42 +1,44 @@ +import { CommonModule } from '@angular/common'; import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { CompanyService } from 'src/app/services/company.service'; -import { MatButtonModule } from '@angular/material/button'; -import { CommonModule } from '@angular/common'; @Component({ - selector: 'app-delete-member-dialog', - templateUrl: './delete-member-dialog.component.html', - styleUrls: ['./delete-member-dialog.component.css'], - imports: [ - CommonModule, - MatDialogModule, - MatButtonModule, - ] + selector: 'app-delete-member-dialog', + templateUrl: './delete-member-dialog.component.html', + styleUrls: ['./delete-member-dialog.component.css'], + imports: [CommonModule, MatDialogModule, MatButtonModule], }) export class DeleteMemberDialogComponent { - public submitting: boolean = false; + public submitting: boolean = false; - constructor( - @Inject(MAT_DIALOG_DATA) public data: any, - public dialogRef: MatDialogRef, - private _company: CompanyService, - private angulartics2: Angulartics2, - ) { } + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + public dialogRef: MatDialogRef, + private _company: CompanyService, + private angulartics2: Angulartics2, + ) {} - deleteCompanyMember() { - this.submitting = true; - this._company.removeCompanyMemder(this.data.companyId, this.data.user.id, this.data.user.email, this.data.user.name) - .subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Company: member is deleted successfully', - }); - this.submitting = false; - this.dialogRef.close(); - }, - () => {}, - () => {this.submitting = false} - ); - } + deleteCompanyMember() { + this.submitting = true; + this._company + .removeCompanyMemder(this.data.companyId, this.data.user.id, this.data.user.email, this.data.user.name) + .subscribe( + () => { + this.angulartics2.eventTrack.next({ + action: 'Company: member is deleted successfully', + }); + posthog.capture('Company: member is deleted successfully'); + this.submitting = false; + this.dialogRef.close(); + }, + () => {}, + () => { + this.submitting = false; + }, + ); + } } diff --git a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html index 299490e5e..8445e7932 100644 --- a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html +++ b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html @@ -12,7 +12,8 @@

Add member to {{company.name}} company

+ angularticsAction="Company: role is clicked" + (click)="posthog.capture('Company: role is clicked')"> {{ companyRolesName[companyMemberRole] }} arrow_drop_down diff --git a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts index 04f75f9c0..d472a0d5c 100644 --- a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts +++ b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts @@ -9,6 +9,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; import { MatSelectModule } from '@angular/material/select'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive'; import { CompanyMemberRole } from 'src/app/models/company'; import { CompanyService } from 'src/app/services/company.service'; @@ -36,6 +37,7 @@ import { TurnstileComponent } from '../../ui-components/turnstile/turnstile.comp ], }) export class InviteMemberDialogComponent { + protected posthog = posthog; @ViewChild(TurnstileComponent) turnstileWidget: TurnstileComponent; CompanyMemberRole = CompanyMemberRole; @@ -83,6 +85,7 @@ export class InviteMemberDialogComponent { this.angulartics2.eventTrack.next({ action: 'Company: member is invited successfully', }); + posthog.capture('Company: member is invited successfully'); this.submitting = false; this.dialogRef.close(); diff --git a/frontend/src/app/components/company/revoke-invitation-dialog/revoke-invitation-dialog.component.ts b/frontend/src/app/components/company/revoke-invitation-dialog/revoke-invitation-dialog.component.ts index e91a611ec..93a8b9a29 100644 --- a/frontend/src/app/components/company/revoke-invitation-dialog/revoke-invitation-dialog.component.ts +++ b/frontend/src/app/components/company/revoke-invitation-dialog/revoke-invitation-dialog.component.ts @@ -1,42 +1,42 @@ +import { CommonModule } from '@angular/common'; import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; import { MatButtonModule } from '@angular/material/button'; -import { CommonModule } from '@angular/common'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { CompanyService } from 'src/app/services/company.service'; @Component({ - selector: 'app-revoke-invitation-dialog', - templateUrl: './revoke-invitation-dialog.component.html', - styleUrls: ['./revoke-invitation-dialog.component.css'], - imports: [ - CommonModule, - MatDialogModule, - MatButtonModule - ] + selector: 'app-revoke-invitation-dialog', + templateUrl: './revoke-invitation-dialog.component.html', + styleUrls: ['./revoke-invitation-dialog.component.css'], + imports: [CommonModule, MatDialogModule, MatButtonModule], }) export class RevokeInvitationDialogComponent { - public submitting: boolean = false; + public submitting: boolean = false; - constructor( - @Inject(MAT_DIALOG_DATA) public data: any, - public dialogRef: MatDialogRef, - private _company: CompanyService, - private angulartics2: Angulartics2, - ) { } + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + public dialogRef: MatDialogRef, + private _company: CompanyService, + private angulartics2: Angulartics2, + ) {} - revokeInvitation() { - this._company.revokeInvitation(this.data.companyId, this.data.userEmail) - .subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Company: invitation is revoked successfully', - }); + revokeInvitation() { + this._company.revokeInvitation(this.data.companyId, this.data.userEmail).subscribe( + () => { + this.angulartics2.eventTrack.next({ + action: 'Company: invitation is revoked successfully', + }); + posthog.capture('Company: invitation is revoked successfully'); - this.submitting = false; - this.dialogRef.close(); - }, - () => {}, - () => {this.submitting = false} - ); - } + this.submitting = false; + this.dialogRef.close(); + }, + () => {}, + () => { + this.submitting = false; + }, + ); + } } diff --git a/frontend/src/app/components/connect-db/connect-db.component.html b/frontend/src/app/components/connect-db/connect-db.component.html index 09ccab159..fc00cb029 100644 --- a/frontend/src/app/components/connect-db/connect-db.component.html +++ b/frontend/src/app/components/connect-db/connect-db.component.html @@ -14,6 +14,7 @@

data-testid="connection-title-input" angulartics2On="change" angularticsAction="Connection creds {{ db.id ? 'edit' : 'add' }}: title is edited" + (change)="posthog.capture('Connection creds {{ db.id ? \'edit\' : \'add\' }}: title is edited')" [required]="db.connectionType === 'agent'" [readonly]="(accessLevel === 'readonly' || db.isTestConnection) && db.id" [disabled]="submitting" @@ -26,6 +27,7 @@

data-testid="connection-dbtype-select" angulartics2On="click" angularticsAction="Connection creds {{ db.id ? 'edit' : 'add' }}: db type is selected" + (click)="posthog.capture('Connection creds {{ db.id ? \'edit\' : \'add\' }}: db type is selected')" [angularticsProperties]="{'dbType': db.type}" [disabled]="submitting" [(ngModel)]="db.type" @@ -232,7 +234,8 @@

href="https://hub.docker.com/r/rocketadmin/rocketadmin-agent" target="_blank" data-testid="connection-agent-dockerhub-link" angulartics2On="click" - angularticsAction="Connection creds {{ db.id ? 'edit' : 'add' }}: open github agent page"> + angularticsAction="Connection creds {{ db.id ? 'edit' : 'add' }}: open github agent page" + (click)="posthog.capture('Connection creds {{ db.id ? \'edit\' : \'add\' }}: open github agent page')"> target="_blank" data-testid="connection-agent-instruction-link" angulartics2On="click" - angularticsAction="Connection creds {{ db.id ? 'edit' : 'add' }}: open agent connection instruction"> + angularticsAction="Connection creds {{ db.id ? 'edit' : 'add' }}: open agent connection instruction" + (click)="posthog.capture('Connection creds {{ db.id ? \'edit\' : \'add\' }}: open agent connection instruction')"> Check out the complete guide @@ -292,7 +296,7 @@

angulartics2On="click" angularticsAction="Connection creds test: delete db is clicked" [disabled]="submitting" - (click)="confirmDeleteConnection(db, $event)"> + (click)="confirmDeleteConnection(db, $event); posthog.capture('Connection creds test: delete db is clicked')"> Delete @@ -308,7 +312,7 @@

data-testid="add-connection-actions-test-button" angulartics2On="click" angularticsAction="Connection creds {{ db.id ? 'edit' : 'add' }}: test db creds is clicked" - (click)="testConnection()" + (click)="testConnection(); posthog.capture('Connection creds {{ db.id ? \'edit\' : \'add\' }}: test db creds is clicked')" [disabled]="submitting"> Test connection @@ -317,6 +321,7 @@

color="primary" angulartics2On="click" angularticsAction="Connection creds add: db add clicked" + (click)="posthog.capture('Connection creds add: db add clicked')" [disabled]="submitting"> {{ submitting ? 'Checking' : 'Add'}} @@ -335,14 +340,14 @@

angulartics2On="click" angularticsAction="Connection creds edit: delete db is clicked" [disabled]="submitting" - (click)="confirmDeleteConnection(db, $event)"> + (click)="confirmDeleteConnection(db, $event); posthog.capture('Connection creds edit: delete db is clicked')"> Delete @@ -353,6 +358,7 @@

data-testid="edit-connection-actions-edit-button" angulartics2On="click" angularticsAction="Connection creds edit: db edit is clicked" + (click)="posthog.capture('Connection creds edit: db edit is clicked')" [disabled]="submitting || db.isTestConnection"> {{ submitting ? 'Checking' : 'Edit'}} 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 b85af6dbb..c73809296 100644 --- a/frontend/src/app/components/connect-db/connect-db.component.ts +++ b/frontend/src/app/components/connect-db/connect-db.component.ts @@ -1,423 +1,465 @@ -import * as ipaddr from 'ipaddr.js'; - -import { Alert, AlertActionType, AlertType } from 'src/app/models/alert'; -import { Angulartics2, Angulartics2Module } from 'angulartics2'; +import { CdkCopyToClipboard } from '@angular/cdk/clipboard'; +import { CommonModule } from '@angular/common'; import { Component, NgZone, OnInit } from '@angular/core'; -import { Connection, ConnectionType, DBtype, TestConnection } from 'src/app/models/connection'; +import { FormsModule, NgForm } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +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 { Angulartics2, Angulartics2Module } from 'angulartics2'; +import * as ipaddr from 'ipaddr.js'; +import posthog from 'posthog-js'; import { take } from 'rxjs'; import { supportedDatabasesTitles, supportedOrderedDatabases } from 'src/app/consts/databases'; - +import googlIPsList from 'src/app/consts/google-IP-addresses'; +import { Alert, AlertActionType, AlertType } from 'src/app/models/alert'; +import { Connection, ConnectionType, DBtype, TestConnection } from 'src/app/models/connection'; import { AccessLevel } from 'src/app/models/user'; -import { AlertComponent } from '../ui-components/alert/alert.component'; -import { CassandraCredentialsFormComponent } from './db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component'; -import { CdkCopyToClipboard } from '@angular/cdk/clipboard'; -import { CommonModule } from '@angular/common'; import { CompanyService } from 'src/app/services/company.service'; import { ConnectionsService } from 'src/app/services/connections.service'; -import { Db2CredentialsFormComponent } from './db-credentials-forms/db2-credentials-form/db2-credentials-form.component'; +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 { 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'; 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 { CassandraCredentialsFormComponent } from './db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component'; +import { ClickhouseCredentialsFormComponent } from './db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component'; +import { Db2CredentialsFormComponent } from './db-credentials-forms/db2-credentials-form/db2-credentials-form.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'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDialog } from '@angular/material/dialog'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { MongodbCredentialsFormComponent } from './db-credentials-forms/mongodb-credentials-form/mongodb-credentials-form.component'; import { MssqlCredentialsFormComponent } from './db-credentials-forms/mssql-credentials-form/mssql-credentials-form.component'; import { MysqlCredentialsFormComponent } from './db-credentials-forms/mysql-credentials-form/mysql-credentials-form.component'; -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 { ClickhouseCredentialsFormComponent } from './db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component'; @Component({ - selector: 'app-connect-db', - templateUrl: './connect-db.component.html', - styleUrls: ['./connect-db.component.css'], - imports: [ - MatInputModule, - MatSelectModule, - MatButtonToggleModule, - MatIconModule, - MatButtonModule, - MatTooltipModule, - CdkCopyToClipboard, - RouterModule, - FormsModule, - CommonModule, - MatDialogModule, - MatCheckboxModule, - MatSlideToggleModule, - Db2CredentialsFormComponent, - DynamodbCredentialsFormComponent, - CassandraCredentialsFormComponent, - MongodbCredentialsFormComponent, - MssqlCredentialsFormComponent, - MysqlCredentialsFormComponent, - OracledbCredentialsFormComponent, - PostgresCredentialsFormComponent, - RedisCredentialsFormComponent, - ElasticCredentialsFormComponent, - ClickhouseCredentialsFormComponent, - IpAddressButtonComponent, - AlertComponent, - Angulartics2Module - ] + selector: 'app-connect-db', + templateUrl: './connect-db.component.html', + styleUrls: ['./connect-db.component.css'], + imports: [ + MatInputModule, + MatSelectModule, + MatButtonToggleModule, + MatIconModule, + MatButtonModule, + MatTooltipModule, + CdkCopyToClipboard, + RouterModule, + FormsModule, + CommonModule, + MatDialogModule, + MatCheckboxModule, + MatSlideToggleModule, + Db2CredentialsFormComponent, + DynamodbCredentialsFormComponent, + CassandraCredentialsFormComponent, + MongodbCredentialsFormComponent, + MssqlCredentialsFormComponent, + MysqlCredentialsFormComponent, + OracledbCredentialsFormComponent, + PostgresCredentialsFormComponent, + RedisCredentialsFormComponent, + ElasticCredentialsFormComponent, + ClickhouseCredentialsFormComponent, + IpAddressButtonComponent, + AlertComponent, + Angulartics2Module, + ], }) export class ConnectDBComponent implements OnInit { - - public isSaas = (environment as any).saas; - public connectionID: string | null = null; - public masterKey: string; - public connectionToken: string | null = null; - public submitting: boolean = false; - public otherOS = []; - public warning: Alert = { - id: 10000000, - type: AlertType.Warning, - message: null - } - - public supportedOrderedDatabases = supportedOrderedDatabases; - public supportedDatabasesTitles = supportedDatabasesTitles; - public ports = { - [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' - } - - // public isDemo: boolean = false; - - public isDemoConnectionWarning: Alert = { - id: 10000000, - type: AlertType.Warning, - message: '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.' - } - - constructor( - private _connections: ConnectionsService, - private _notifications: NotificationsService, - public _user: UserService, - private _company: CompanyService, - private ngZone: NgZone, - public router: Router, - public dialog: MatDialog, - private angulartics2: Angulartics2, - private title: Title - ) { } - - get isDemo() { - return this._user.isDemo; - } - - ngOnInit() { - this.connectionID = this._connections.currentConnectionID; - - const databaseType = this.router.routerState.snapshot.root.queryParams.type; - - if (databaseType) { - this.db.type = databaseType - this.db.port = this.ports[databaseType]; - }; - - this._connections.getCurrentConnectionTitle() - .pipe(take(1)) - .subscribe(connectionTitle => { - if (this.connectionID) { - this.title.setTitle(`Credentials — ${connectionTitle} | ${this._company.companyTabTitle || 'Rocketadmin'}`); - } else { - this.title.setTitle(`Add new database | ${this._company.companyTabTitle || 'Rocketadmin'}`); - } - }); - - if (!this.connectionID) { - this._user.sendUserAction('CONNECTION_CREATION_NOT_FINISHED').subscribe(); - if (this.isSaas) { - // @ts-expect-error - fbq('trackCustom', 'Add_connection'); - } - }; - } - - get db():Connection { - return this._connections.currentConnection; - } - - get accessLevel():AccessLevel { - return this._connections.currentConnectionAccessLevel; - } - - dbTypeChange() { - this.db.port = this.ports[this.db.type]; - } - - testConnection() { - this.submitting = true; - this._connections.testConnection(this.connectionID, this.db) - .subscribe( - (credsCorrect: TestConnection) => { - this.angulartics2.eventTrack.next({ - action: `Connect DB: manual test connection before ${this.db.id ? 'edit' : 'add'} is ${credsCorrect.result ? 'passed' : 'failed'}`, - properties: { errorMessage: credsCorrect.message } - }); - if (credsCorrect.result) { - this._notifications.dismissAlert(); - this._notifications.showSuccessSnackbar('Connection is live') - } else { - this._notifications.showAlert(AlertType.Error, credsCorrect.message, [ - { - type: AlertActionType.Button, - caption: 'Dismiss', - action: (_id: number) => this._notifications.dismissAlert() - } - ]); - }; - }, - () => {this.submitting = false}, - () => {this.submitting = false} - ); - } - - createConnectionRequest() { - this._connections.createConnection(this.db, this.masterKey) - .subscribe((res: any) => { - this.ngZone.run(() => { - const createdConnectionID = res.id!; - if (this.db.connectionType === 'agent') { - this.connectionToken = res.token; - this.connectionID = res.id; - } else { - this.router.navigate([`/dashboard/${createdConnectionID}`]); - }; - this.angulartics2.eventTrack.next({ - action: 'Connect DB: connection is added successfully', - properties: { connectionType: this.db.connectionType, dbType: this.db.type } - }); - }); - }, - (errorMessage) => { - this.angulartics2.eventTrack.next({ - action: 'Connect DB: connection is added unsuccessfully', - properties: { connectionType: this.db.connectionType, dbType: this.db.type, errorMessage } - }); - this.submitting = false; - }, - () => {this.submitting = false} - ) - } - - updateConnectionRequest() { - this._connections.updateConnection(this.db, this.masterKey) - .subscribe((res: any) => { - this.ngZone.run(() => { - const connectionID = res.connection.id!; - if (this.db.connectionType === 'agent') { - this.connectionToken = res.connection.token; - this.angulartics2.eventTrack.next({ - action: 'Connect DB: connection is edited successfully', - properties: { connectionType: 'agent' } - }); - } else { - this.angulartics2.eventTrack.next({ - action: 'Connect DB: connection is edited successfully', - properties: { connectionType: 'direct' } - }); - this.router.navigate([`/dashboard/${connectionID}`]); - }; - }); - }, - (errorMessage) => { - this.angulartics2.eventTrack.next({ - action: 'Connect DB: connection is edited unsuccessfully', - properties: { errorMessage } - }); - this.submitting = false; - }, () => { - this.submitting = false; - }) - } - - handleConnectionError(errorMessage: string) { - this.dialog.open(DbConnectionConfirmDialogComponent, { - width: '32em', - data: { - dbCreds: this.db, - provider: this.getProvider(), - masterKey: this.masterKey, - errorMessage - } - }); - this.submitting = false; - } - - handleCredentialsSubmitting(connectForm: NgForm) { - this.db.masterEncryption = !!this.masterKey; - if (this.db.id) { - this.editConnection(); - } else { - this.createConnection(connectForm); - } - } - - async editConnection() { - this.submitting = true; - let credsCorrect: TestConnection; - - (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 } - }); - - if ((this.db.connectionType === 'agent' || credsCorrect.result)) { - this.updateConnectionRequest(); - } else { - this.handleConnectionError(credsCorrect.message); - }; - } - - createConnection(_connectForm: NgForm) { - if (this.db.connectionType === 'direct') { - if (this.db.type !== DBtype.Dynamo) { - const ipAddressDilaog = this.dialog.open(DbConnectionIpAccessDialogComponent, { - width: '36em', - data: { - db: this.db, - provider: this.getProvider() - } - }); - - ipAddressDilaog.afterClosed().subscribe( async (action) => { - if (action === 'confirmed') { - this.submitting = true; - let credsCorrect: TestConnection = null; - - try { - (credsCorrect as any) = await this._connections.testConnection(this.connectionID, this.db).toPromise(); - - this.angulartics2.eventTrack.next({ - action: `Connect DB: automatic test connection on add is ${credsCorrect.result ? 'passed' : 'failed'}`, - properties: { connectionType: this.db.connectionType, dbType: this.db.type, errorMessage: credsCorrect.message } - }); - - if (credsCorrect?.result) { - this.createConnectionRequest(); - } else { - this.handleConnectionError(credsCorrect.message); - }; - } catch (_e) { - credsCorrect = null; - this.submitting = false; - } - } - }) - } else { - this.submitting = true; - let credsCorrect: TestConnection = null; - - this._connections.testConnection(this.connectionID, this.db).toPromise() - .then((res: TestConnection) => { - credsCorrect = res; - this.angulartics2.eventTrack.next({ - action: `Connect DB: automatic test connection on add is ${credsCorrect.result ? 'passed' : 'failed'}`, - properties: { - connectionType: this.db.connectionType, - dbType: this.db.type, - errorMessage: credsCorrect.message - } - }); - if (credsCorrect?.result) { - this.createConnectionRequest(); - } else { - this.handleConnectionError(credsCorrect.message); - } - }) - .catch((_e) => { - credsCorrect = null; - this.submitting = false; - }); - } - - } else { - this.createConnectionRequest(); - } - } - - confirmDeleteConnection(connectionCreds: any, event: Event): void { - event.preventDefault(); - event.stopImmediatePropagation(); - this.dialog.open(DbConnectionDeleteDialogComponent, { - width: '32em', - data: connectionCreds - }); - } - - generatePassword (checked: boolean) { - if (checked) { - let randomArray = new Uint8Array(32); - window.crypto.getRandomValues(randomArray); - this.masterKey = btoa(String.fromCharCode(...randomArray)); - } - } - - showCopyNotification(message: string) { - this._notifications.showSuccessSnackbar(message); - } - - switchToAgent() { - console.log('switchToAgent'); - this.db.connectionType = ConnectionType.Agent; - } - - handleMasterKeyChange(newMasterKey: string): void { - this.masterKey = newMasterKey; - } - - getProvider() { - let provider: string = null; - if (this.db.host.endsWith('.amazonaws.com')) provider = 'amazon'; - if (this.db.host.endsWith('.amazonaws.com') && this.db.type === DBtype.Dynamo) provider = 'amazonDynamoDB'; - if (this.db.host.endsWith('.azure.com')) provider = 'azure'; - if (this.db.host.endsWith('.mongodb.net')) provider = 'mongoatlas'; - if (this.db.host.endsWith('.ondigitalocean.com')) provider = 'digitalocean'; - if (this.db.host.endsWith('.scylla.cloud')) provider = 'scylladbcloud'; - if(isIP(this.db.host)) { - const hostIP = ipaddr.parse(this.db.host); - for (const addr of googlIPsList) { - if (hostIP.match(ipaddr.parseCIDR(addr))) { - provider = 'google'; - return; - } - } - } - return provider; - } + protected posthog = posthog; + + public isSaas = (environment as any).saas; + public connectionID: string | null = null; + public masterKey: string; + public connectionToken: string | null = null; + public submitting: boolean = false; + public otherOS = []; + public warning: Alert = { + id: 10000000, + type: AlertType.Warning, + message: null, + }; + + public supportedOrderedDatabases = supportedOrderedDatabases; + public supportedDatabasesTitles = supportedDatabasesTitles; + public ports = { + [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', + }; + + // public isDemo: boolean = false; + + public isDemoConnectionWarning: Alert = { + id: 10000000, + type: AlertType.Warning, + message: + "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.", + }; + + constructor( + private _connections: ConnectionsService, + private _notifications: NotificationsService, + public _user: UserService, + private _company: CompanyService, + private ngZone: NgZone, + public router: Router, + public dialog: MatDialog, + private angulartics2: Angulartics2, + private title: Title, + ) {} + + get isDemo() { + return this._user.isDemo; + } + + ngOnInit() { + this.connectionID = this._connections.currentConnectionID; + + const databaseType = this.router.routerState.snapshot.root.queryParams.type; + + if (databaseType) { + this.db.type = databaseType; + this.db.port = this.ports[databaseType]; + } + + this._connections + .getCurrentConnectionTitle() + .pipe(take(1)) + .subscribe((connectionTitle) => { + if (this.connectionID) { + this.title.setTitle(`Credentials — ${connectionTitle} | ${this._company.companyTabTitle || 'Rocketadmin'}`); + } else { + this.title.setTitle(`Add new database | ${this._company.companyTabTitle || 'Rocketadmin'}`); + } + }); + + if (!this.connectionID) { + this._user.sendUserAction('CONNECTION_CREATION_NOT_FINISHED').subscribe(); + if (this.isSaas) { + // @ts-expect-error + fbq('trackCustom', 'Add_connection'); + } + } + } + + get db(): Connection { + return this._connections.currentConnection; + } + + get accessLevel(): AccessLevel { + return this._connections.currentConnectionAccessLevel; + } + + dbTypeChange() { + this.db.port = this.ports[this.db.type]; + } + + testConnection() { + this.submitting = true; + this._connections.testConnection(this.connectionID, this.db).subscribe( + (credsCorrect: TestConnection) => { + this.angulartics2.eventTrack.next({ + action: `Connect DB: manual test connection before ${this.db.id ? 'edit' : 'add'} is ${credsCorrect.result ? 'passed' : 'failed'}`, + properties: { errorMessage: credsCorrect.message }, + }); + posthog.capture( + `Connect DB: manual test connection before ${this.db.id ? 'edit' : 'add'} is ${credsCorrect.result ? 'passed' : 'failed'}`, + { errorMessage: credsCorrect.message }, + ); + if (credsCorrect.result) { + this._notifications.dismissAlert(); + this._notifications.showSuccessSnackbar('Connection is live'); + } else { + this._notifications.showAlert(AlertType.Error, credsCorrect.message, [ + { + type: AlertActionType.Button, + caption: 'Dismiss', + action: (_id: number) => this._notifications.dismissAlert(), + }, + ]); + } + }, + () => { + this.submitting = false; + }, + () => { + this.submitting = false; + }, + ); + } + + createConnectionRequest() { + this._connections.createConnection(this.db, this.masterKey).subscribe( + (res: any) => { + this.ngZone.run(() => { + const createdConnectionID = res.id!; + if (this.db.connectionType === 'agent') { + this.connectionToken = res.token; + this.connectionID = res.id; + } else { + this.router.navigate([`/dashboard/${createdConnectionID}`]); + } + this.angulartics2.eventTrack.next({ + action: 'Connect DB: connection is added successfully', + properties: { connectionType: this.db.connectionType, dbType: this.db.type }, + }); + posthog.capture('Connect DB: connection is added successfully', { + connectionType: this.db.connectionType, + dbType: this.db.type, + }); + }); + }, + (errorMessage) => { + this.angulartics2.eventTrack.next({ + action: 'Connect DB: connection is added unsuccessfully', + properties: { connectionType: this.db.connectionType, dbType: this.db.type, errorMessage }, + }); + posthog.capture('Connect DB: connection is added unsuccessfully', { + connectionType: this.db.connectionType, + dbType: this.db.type, + errorMessage, + }); + this.submitting = false; + }, + () => { + this.submitting = false; + }, + ); + } + + updateConnectionRequest() { + this._connections.updateConnection(this.db, this.masterKey).subscribe( + (res: any) => { + this.ngZone.run(() => { + const connectionID = res.connection.id!; + if (this.db.connectionType === 'agent') { + this.connectionToken = res.connection.token; + this.angulartics2.eventTrack.next({ + action: 'Connect DB: connection is edited successfully', + properties: { connectionType: 'agent' }, + }); + posthog.capture('Connect DB: connection is edited successfully', { connectionType: 'agent' }); + } else { + this.angulartics2.eventTrack.next({ + action: 'Connect DB: connection is edited successfully', + properties: { connectionType: 'direct' }, + }); + posthog.capture('Connect DB: connection is edited successfully', { connectionType: 'direct' }); + this.router.navigate([`/dashboard/${connectionID}`]); + } + }); + }, + (errorMessage) => { + this.angulartics2.eventTrack.next({ + action: 'Connect DB: connection is edited unsuccessfully', + properties: { errorMessage }, + }); + posthog.capture('Connect DB: connection is edited unsuccessfully', { errorMessage }); + this.submitting = false; + }, + () => { + this.submitting = false; + }, + ); + } + + handleConnectionError(errorMessage: string) { + this.dialog.open(DbConnectionConfirmDialogComponent, { + width: '32em', + data: { + dbCreds: this.db, + provider: this.getProvider(), + masterKey: this.masterKey, + errorMessage, + }, + }); + this.submitting = false; + } + + handleCredentialsSubmitting(connectForm: NgForm) { + this.db.masterEncryption = !!this.masterKey; + if (this.db.id) { + this.editConnection(); + } else { + this.createConnection(connectForm); + } + } + + async editConnection() { + this.submitting = true; + let credsCorrect: TestConnection; + + (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 }, + }); + posthog.capture(`Connect DB: automatic test connection on edit is ${credsCorrect?.result ? 'passed' : 'failed'}`, { + errorMessage: credsCorrect?.message, + }); + + if (this.db.connectionType === 'agent' || credsCorrect.result) { + this.updateConnectionRequest(); + } else { + this.handleConnectionError(credsCorrect.message); + } + } + + createConnection(_connectForm: NgForm) { + if (this.db.connectionType === 'direct') { + if (this.db.type !== DBtype.Dynamo) { + const ipAddressDilaog = this.dialog.open(DbConnectionIpAccessDialogComponent, { + width: '36em', + data: { + db: this.db, + provider: this.getProvider(), + }, + }); + + ipAddressDilaog.afterClosed().subscribe(async (action) => { + if (action === 'confirmed') { + this.submitting = true; + let credsCorrect: TestConnection = null; + + try { + (credsCorrect as any) = await this._connections.testConnection(this.connectionID, this.db).toPromise(); + + this.angulartics2.eventTrack.next({ + action: `Connect DB: automatic test connection on add is ${credsCorrect.result ? 'passed' : 'failed'}`, + properties: { + connectionType: this.db.connectionType, + dbType: this.db.type, + errorMessage: credsCorrect.message, + }, + }); + posthog.capture( + `Connect DB: automatic test connection on add is ${credsCorrect.result ? 'passed' : 'failed'}`, + { connectionType: this.db.connectionType, dbType: this.db.type, errorMessage: credsCorrect.message }, + ); + + if (credsCorrect?.result) { + this.createConnectionRequest(); + } else { + this.handleConnectionError(credsCorrect.message); + } + } catch (_e) { + credsCorrect = null; + this.submitting = false; + } + } + }); + } else { + this.submitting = true; + let credsCorrect: TestConnection = null; + + this._connections + .testConnection(this.connectionID, this.db) + .toPromise() + .then((res: TestConnection) => { + credsCorrect = res; + this.angulartics2.eventTrack.next({ + action: `Connect DB: automatic test connection on add is ${credsCorrect.result ? 'passed' : 'failed'}`, + properties: { + connectionType: this.db.connectionType, + dbType: this.db.type, + errorMessage: credsCorrect.message, + }, + }); + posthog.capture( + `Connect DB: automatic test connection on add is ${credsCorrect.result ? 'passed' : 'failed'}`, + { + connectionType: this.db.connectionType, + dbType: this.db.type, + errorMessage: credsCorrect.message, + }, + ); + if (credsCorrect?.result) { + this.createConnectionRequest(); + } else { + this.handleConnectionError(credsCorrect.message); + } + }) + .catch((_e) => { + credsCorrect = null; + this.submitting = false; + }); + } + } else { + this.createConnectionRequest(); + } + } + + confirmDeleteConnection(connectionCreds: any, event: Event): void { + event.preventDefault(); + event.stopImmediatePropagation(); + this.dialog.open(DbConnectionDeleteDialogComponent, { + width: '32em', + data: connectionCreds, + }); + } + + generatePassword(checked: boolean) { + if (checked) { + let randomArray = new Uint8Array(32); + window.crypto.getRandomValues(randomArray); + this.masterKey = btoa(String.fromCharCode(...randomArray)); + } + } + + showCopyNotification(message: string) { + this._notifications.showSuccessSnackbar(message); + } + + switchToAgent() { + console.log('switchToAgent'); + this.db.connectionType = ConnectionType.Agent; + } + + handleMasterKeyChange(newMasterKey: string): void { + this.masterKey = newMasterKey; + } + + getProvider() { + let provider: string = null; + if (this.db.host.endsWith('.amazonaws.com')) provider = 'amazon'; + if (this.db.host.endsWith('.amazonaws.com') && this.db.type === DBtype.Dynamo) provider = 'amazonDynamoDB'; + if (this.db.host.endsWith('.azure.com')) provider = 'azure'; + if (this.db.host.endsWith('.mongodb.net')) provider = 'mongoatlas'; + if (this.db.host.endsWith('.ondigitalocean.com')) provider = 'digitalocean'; + if (this.db.host.endsWith('.scylla.cloud')) provider = 'scylladbcloud'; + if (isIP(this.db.host)) { + const hostIP = ipaddr.parse(this.db.host); + for (const addr of googlIPsList) { + if (hostIP.match(ipaddr.parseCIDR(addr))) { + provider = 'google'; + return; + } + } + } + return provider; + } } diff --git a/frontend/src/app/components/connect-db/db-connection-delete-dialog/db-connection-delete-dialog.component.ts b/frontend/src/app/components/connect-db/db-connection-delete-dialog/db-connection-delete-dialog.component.ts index 87e4663f1..077a39e6a 100644 --- a/frontend/src/app/components/connect-db/db-connection-delete-dialog/db-connection-delete-dialog.component.ts +++ b/frontend/src/app/components/connect-db/db-connection-delete-dialog/db-connection-delete-dialog.component.ts @@ -1,70 +1,70 @@ -import { Component, OnInit, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; -import { Router } from '@angular/router'; -import { Angulartics2 } from 'angulartics2'; -import { ConnectionsService } from 'src/app/services/connections.service'; +import { Component, Inject, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; +import { Router } from '@angular/router'; +import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; +import { ConnectionsService } from 'src/app/services/connections.service'; @Component({ - selector: 'app-db-connection-delete-dialog', - templateUrl: './db-connection-delete-dialog.component.html', - styleUrls: ['./db-connection-delete-dialog.component.css'], - imports: [ - MatDialogModule, - FormsModule, - MatButtonModule, - MatFormFieldModule, - MatInputModule, - MatRadioModule - ] + selector: 'app-db-connection-delete-dialog', + templateUrl: './db-connection-delete-dialog.component.html', + styleUrls: ['./db-connection-delete-dialog.component.css'], + imports: [MatDialogModule, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatRadioModule], }) export class DbConnectionDeleteDialogComponent implements OnInit { + public connectionName: string; + public reason: string; + public message: string; + public submitting: boolean = false; - public connectionName: string; - public reason: string; - public message: string; - public submitting: boolean = false; - - constructor( - @Inject(MAT_DIALOG_DATA) public data: any, - private _connectionsService: ConnectionsService, - public dialogRef: MatDialogRef, - public router: Router, - private angulartics2: Angulartics2 - ) { } - - ngOnInit() { - this.connectionName = this.data.title || this.data.database; - } + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + private _connectionsService: ConnectionsService, + public dialogRef: MatDialogRef, + public router: Router, + private angulartics2: Angulartics2, + ) {} - deleteConnection() { - this.submitting = true; - const metadata = { - reason: this.reason, - message: this.message - } - this._connectionsService.deleteConnection(this.data.id, metadata) - .subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Connect DB: connection is deleted', - properties: { - id: this.data.id, - title: this.data.title || null, - reason: this.reason, - message: this.message - } - }); - this.submitting = false; - this.router.navigate([`/connections-list`]); - this.dialogRef.close(); - }, - undefined, - () => { this.submitting = false; } - ); - } + ngOnInit() { + this.connectionName = this.data.title || this.data.database; + } + deleteConnection() { + this.submitting = true; + const metadata = { + reason: this.reason, + message: this.message, + }; + this._connectionsService.deleteConnection(this.data.id, metadata).subscribe( + () => { + this.angulartics2.eventTrack.next({ + action: 'Connect DB: connection is deleted', + properties: { + id: this.data.id, + title: this.data.title || null, + reason: this.reason, + message: this.message, + }, + }); + posthog.capture('Connect DB: connection is deleted', { + id: this.data.id, + title: this.data.title || null, + reason: this.reason, + message: this.message, + }); + this.submitting = false; + this.router.navigate([`/connections-list`]); + this.dialogRef.close(); + }, + undefined, + () => { + this.submitting = false; + }, + ); + } } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component.html index b340a513d..11eefefb4 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -29,6 +30,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -42,6 +44,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -55,6 +58,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('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" @@ -69,6 +73,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -82,6 +87,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database dataCenter is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database dataCenter is edited')" [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" [(ngModel)]="connection.dataCenter"> @@ -107,6 +113,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -120,6 +127,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -133,6 +141,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -145,6 +154,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -158,6 +168,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -169,6 +180,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -181,6 +193,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component.ts index a0896f29e..6b55e8ee9 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/cassandra-credentials-form/cassandra-credentials-form.component.ts @@ -1,32 +1,35 @@ +import { NgIf } from '@angular/common'; import { Component } from '@angular/core'; -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; 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'; -import { MatCheckboxModule } from '@angular/material/checkbox'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @Component({ - selector: 'app-cassandra-credentials-form', - templateUrl: './cassandra-credentials-form.component.html', - styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './cassandra-credentials-form.component.css'], - imports: [ - NgIf, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatCheckboxModule, - MatExpansionModule, - HostnameValidationDirective, - MasterEncryptionPasswordComponent, - Angulartics2Module - ] + selector: 'app-cassandra-credentials-form', + templateUrl: './cassandra-credentials-form.component.html', + styleUrls: [ + '../base-credentials-form/base-credentials-form.component.css', + './cassandra-credentials-form.component.css', + ], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatCheckboxModule, + MatExpansionModule, + HostnameValidationDirective, + MasterEncryptionPasswordComponent, + Angulartics2Module, + ], }) - export class CassandraCredentialsFormComponent extends BaseCredentialsFormComponent { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component.html index e65949a91..7b4244cd1 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator [readonly]="readonly" [disabled]="submitting" @@ -28,6 +29,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -41,6 +43,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -54,6 +57,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: password is edited')" [required]="!connection.id || hostname.touched || port.touched" [readonly]="readonly" [disabled]="submitting" @@ -69,6 +73,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -96,6 +101,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -109,6 +115,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -122,6 +129,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -134,6 +142,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -147,6 +156,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -158,6 +168,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -170,6 +181,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component.ts index da950e9e9..532e4ef2c 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/clickhouse-credentials-form/clickhouse-credentials-form.component.ts @@ -1,32 +1,36 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @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 - ] + 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 { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/db2-credentials-form/db2-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/db2-credentials-form/db2-credentials-form.component.html index d18753bc1..d2deb6677 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/db2-credentials-form/db2-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/db2-credentials-form/db2-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -27,6 +28,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -40,6 +42,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -53,6 +56,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('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" @@ -68,6 +72,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -81,6 +86,7 @@ data-testid="connection-schema-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database schema is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database schema is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -107,6 +113,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -120,6 +127,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -133,6 +141,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -145,6 +154,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -158,6 +168,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -169,6 +180,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -181,6 +193,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/db2-credentials-form/db2-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/db2-credentials-form/db2-credentials-form.component.ts index 5651876d9..3bc6cb0e9 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/db2-credentials-form/db2-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/db2-credentials-form/db2-credentials-form.component.ts @@ -1,31 +1,32 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @Component({ - selector: 'app-db2-credentials-form', - templateUrl: './db2-credentials-form.component.html', - styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './db2-credentials-form.component.css'], - imports: [ - NgIf, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatCheckboxModule, - MatExpansionModule, - HostnameValidationDirective, - MasterEncryptionPasswordComponent, - Angulartics2Module - ] + selector: 'app-db2-credentials-form', + templateUrl: './db2-credentials-form.component.html', + styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './db2-credentials-form.component.css'], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatCheckboxModule, + MatExpansionModule, + HostnameValidationDirective, + MasterEncryptionPasswordComponent, + Angulartics2Module, + ], }) export class Db2CredentialsFormComponent extends BaseCredentialsFormComponent { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/dynamodb-credentials-form/dynamodb-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/dynamodb-credentials-form/dynamodb-credentials-form.component.html index 83d205213..5a0f1f1a6 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/dynamodb-credentials-form/dynamodb-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/dynamodb-credentials-form/dynamodb-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator="dynamodb" [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -32,6 +33,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -45,6 +47,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: password is edited')" [required]="!connection.id || hostname.touched" [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/dynamodb-credentials-form/dynamodb-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/dynamodb-credentials-form/dynamodb-credentials-form.component.ts index 93683ad0b..5df44d8b1 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/dynamodb-credentials-form/dynamodb-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/dynamodb-credentials-form/dynamodb-credentials-form.component.ts @@ -1,29 +1,33 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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 { MatExpansionModule } from '@angular/material/expansion'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { NgIf } from '@angular/common'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @Component({ - selector: 'app-dynamodb-credentials-form', - templateUrl: './dynamodb-credentials-form.component.html', - styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './dynamodb-credentials-form.component.css'], - imports: [ - NgIf, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatExpansionModule, - HostnameValidationDirective, - MasterEncryptionPasswordComponent, - Angulartics2Module - ] + selector: 'app-dynamodb-credentials-form', + templateUrl: './dynamodb-credentials-form.component.html', + styleUrls: [ + '../base-credentials-form/base-credentials-form.component.css', + './dynamodb-credentials-form.component.css', + ], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatExpansionModule, + HostnameValidationDirective, + MasterEncryptionPasswordComponent, + Angulartics2Module, + ], }) export class DynamodbCredentialsFormComponent extends BaseCredentialsFormComponent { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/elastic-credentials-form/elastic-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/elastic-credentials-form/elastic-credentials-form.component.html index 625dc77dc..ee0576487 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/elastic-credentials-form/elastic-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/elastic-credentials-form/elastic-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -27,6 +28,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -40,6 +42,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -53,6 +56,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('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" @@ -81,6 +85,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" [(ngModel)]="connection.username"> @@ -93,6 +98,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" [(ngModel)]="connection.database"> @@ -104,6 +110,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -117,6 +124,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -130,6 +138,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -142,6 +151,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -155,6 +165,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -166,6 +177,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -178,6 +190,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/elastic-credentials-form/elastic-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/elastic-credentials-form/elastic-credentials-form.component.ts index ee726283f..64fcd6e29 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/elastic-credentials-form/elastic-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/elastic-credentials-form/elastic-credentials-form.component.ts @@ -1,31 +1,35 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @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 - ] + 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 { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/mongodb-credentials-form/mongodb-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/mongodb-credentials-form/mongodb-credentials-form.component.html index da0909c1f..9476bdbf6 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/mongodb-credentials-form/mongodb-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/mongodb-credentials-form/mongodb-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator="mongodb" [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -27,6 +28,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -40,6 +42,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -53,6 +56,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('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" @@ -67,6 +71,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -80,6 +85,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database authSource is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database authSource is edited')" [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" [(ngModel)]="connection.authSource"> @@ -105,6 +111,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -118,6 +125,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -131,6 +139,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -143,6 +152,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -156,6 +166,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -167,6 +178,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -179,6 +191,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/mongodb-credentials-form/mongodb-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/mongodb-credentials-form/mongodb-credentials-form.component.ts index 31acaa3e0..168163db6 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/mongodb-credentials-form/mongodb-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/mongodb-credentials-form/mongodb-credentials-form.component.ts @@ -1,31 +1,35 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @Component({ - selector: 'app-mongodb-credentials-form', - templateUrl: './mongodb-credentials-form.component.html', - styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './mongodb-credentials-form.component.css'], - imports: [ - NgIf, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatCheckboxModule, - MatExpansionModule, - HostnameValidationDirective, - MasterEncryptionPasswordComponent, - Angulartics2Module - ] + selector: 'app-mongodb-credentials-form', + templateUrl: './mongodb-credentials-form.component.html', + styleUrls: [ + '../base-credentials-form/base-credentials-form.component.css', + './mongodb-credentials-form.component.css', + ], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatCheckboxModule, + MatExpansionModule, + HostnameValidationDirective, + MasterEncryptionPasswordComponent, + Angulartics2Module, + ], }) export class MongodbCredentialsFormComponent extends BaseCredentialsFormComponent { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/mssql-credentials-form/mssql-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/mssql-credentials-form/mssql-credentials-form.component.html index 697f00ba2..ec7ae2fc4 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/mssql-credentials-form/mssql-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/mssql-credentials-form/mssql-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator [readonly]="readonly" [disabled]="submitting" @@ -28,6 +29,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -41,6 +43,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -54,6 +57,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: password is edited')" [required]="!connection.id || hostname.touched || port.touched" [readonly]="readonly" [disabled]="submitting" @@ -68,6 +72,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -81,6 +86,7 @@ data-testid="connection-schema-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database schema is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database schema is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -107,6 +113,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -120,6 +127,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -133,6 +141,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -145,6 +154,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -158,6 +168,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -169,6 +180,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -181,6 +193,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" @@ -193,6 +206,7 @@ data-testid="connection-ssl-encryption-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: azure encryption is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: azure encryption is switched')" [angularticsProperties]="{'enable': connection.azure_encryption}" [disabled]="submitting" [(ngModel)]="connection.azure_encryption"> diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/mssql-credentials-form/mssql-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/mssql-credentials-form/mssql-credentials-form.component.ts index 80e82fcd4..0fe301693 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/mssql-credentials-form/mssql-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/mssql-credentials-form/mssql-credentials-form.component.ts @@ -1,31 +1,32 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @Component({ - selector: 'app-mssql-credentials-form', - templateUrl: './mssql-credentials-form.component.html', - styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './mssql-credentials-form.component.css'], - imports: [ - NgIf, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatCheckboxModule, - MatExpansionModule, - HostnameValidationDirective, - MasterEncryptionPasswordComponent, - Angulartics2Module - ] + selector: 'app-mssql-credentials-form', + templateUrl: './mssql-credentials-form.component.html', + styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './mssql-credentials-form.component.css'], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatCheckboxModule, + MatExpansionModule, + HostnameValidationDirective, + MasterEncryptionPasswordComponent, + Angulartics2Module, + ], }) export class MssqlCredentialsFormComponent extends BaseCredentialsFormComponent { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/mysql-credentials-form/mysql-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/mysql-credentials-form/mysql-credentials-form.component.html index 5341f454e..a3cda7b1c 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/mysql-credentials-form/mysql-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/mysql-credentials-form/mysql-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator [readonly]="readonly" [disabled]="submitting" @@ -28,6 +29,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -41,6 +43,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -54,6 +57,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: password is edited')" [required]="!connection.id || hostname.touched || port.touched" [readonly]="readonly" [disabled]="submitting" @@ -69,6 +73,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -96,6 +101,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -109,6 +115,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -122,6 +129,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -134,6 +142,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -147,6 +156,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -158,6 +168,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -170,6 +181,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/mysql-credentials-form/mysql-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/mysql-credentials-form/mysql-credentials-form.component.ts index 5859acb07..86e8624bf 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/mysql-credentials-form/mysql-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/mysql-credentials-form/mysql-credentials-form.component.ts @@ -1,32 +1,33 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @Component({ - selector: 'app-mysql-credentials-form', - templateUrl: './mysql-credentials-form.component.html', - styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './mysql-credentials-form.component.css'], - standalone: true, - imports: [ - NgIf, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatCheckboxModule, - MatExpansionModule, - HostnameValidationDirective, - MasterEncryptionPasswordComponent, - Angulartics2Module - ] + selector: 'app-mysql-credentials-form', + templateUrl: './mysql-credentials-form.component.html', + styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './mysql-credentials-form.component.css'], + standalone: true, + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatCheckboxModule, + MatExpansionModule, + HostnameValidationDirective, + MasterEncryptionPasswordComponent, + Angulartics2Module, + ], }) export class MysqlCredentialsFormComponent extends BaseCredentialsFormComponent { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/oracledb-credentials-form/oracledb-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/oracledb-credentials-form/oracledb-credentials-form.component.html index e7fa45707..4b8d102eb 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/oracledb-credentials-form/oracledb-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/oracledb-credentials-form/oracledb-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator [readonly]="readonly" [disabled]="submitting" @@ -28,6 +29,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -41,6 +43,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -54,6 +57,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: password is edited')" [required]="!connection.id || hostname.touched || port.touched" [readonly]="readonly" [disabled]="submitting" @@ -68,6 +72,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -81,6 +86,7 @@ data-testid="connection-schema-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database schema is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database schema is edited')" [readonly]="readonly" [disabled]="submitting" [(ngModel)]="connection.schema"> @@ -107,6 +113,7 @@ data-testid="connection-sid-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SID is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SID is edited')" [readonly]="readonly" [disabled]="submitting" [(ngModel)]="connection.sid" name="sid" #sid="ngModel"> @@ -117,6 +124,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -130,6 +138,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -143,6 +152,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -155,6 +165,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -168,6 +179,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -179,6 +191,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -191,6 +204,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/oracledb-credentials-form/oracledb-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/oracledb-credentials-form/oracledb-credentials-form.component.ts index b11611409..66cde9b35 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/oracledb-credentials-form/oracledb-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/oracledb-credentials-form/oracledb-credentials-form.component.ts @@ -1,31 +1,35 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @Component({ - selector: 'app-oracledb-credentials-form', - templateUrl: './oracledb-credentials-form.component.html', - styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './oracledb-credentials-form.component.css'], - imports: [ - NgIf, - MatFormFieldModule, - MatInputModule, - MatCheckboxModule, - MatExpansionModule, - FormsModule, - HostnameValidationDirective, - MasterEncryptionPasswordComponent, - Angulartics2Module - ] + selector: 'app-oracledb-credentials-form', + templateUrl: './oracledb-credentials-form.component.html', + styleUrls: [ + '../base-credentials-form/base-credentials-form.component.css', + './oracledb-credentials-form.component.css', + ], + imports: [ + NgIf, + MatFormFieldModule, + MatInputModule, + MatCheckboxModule, + MatExpansionModule, + FormsModule, + HostnameValidationDirective, + MasterEncryptionPasswordComponent, + Angulartics2Module, + ], }) export class OracledbCredentialsFormComponent extends BaseCredentialsFormComponent { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/postgres-credentials-form/postgres-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/postgres-credentials-form/postgres-credentials-form.component.html index e56e17bb9..1c843311a 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/postgres-credentials-form/postgres-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/postgres-credentials-form/postgres-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator [readonly]="readonly" [disabled]="submitting" @@ -30,6 +31,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -43,6 +45,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -56,6 +59,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: password is edited')" [required]="!connection.id || hostname.touched || port.touched" [readonly]="readonly" [disabled]="submitting" @@ -70,6 +74,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" required [readonly]="readonly" [disabled]="submitting" @@ -83,6 +88,7 @@ data-testid="connection-schema-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database schema is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database schema is edited')" [readonly]="readonly" [disabled]="submitting" [(ngModel)]="connection.schema"> @@ -108,6 +114,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -121,6 +128,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -134,6 +142,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -146,6 +155,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -159,6 +169,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -170,6 +181,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -182,6 +194,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [required]="connection.ssl" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/postgres-credentials-form/postgres-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/postgres-credentials-form/postgres-credentials-form.component.ts index 2dbcd3452..8bbcf1b08 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/postgres-credentials-form/postgres-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/postgres-credentials-form/postgres-credentials-form.component.ts @@ -1,31 +1,35 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @Component({ - selector: 'app-postgres-credentials-form', - templateUrl: './postgres-credentials-form.component.html', - styleUrls: ['../base-credentials-form/base-credentials-form.component.css', './postgres-credentials-form.component.css'], - imports: [ - NgIf, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatCheckboxModule, - MatExpansionModule, - HostnameValidationDirective, - MasterEncryptionPasswordComponent, - Angulartics2Module - ] + selector: 'app-postgres-credentials-form', + templateUrl: './postgres-credentials-form.component.html', + styleUrls: [ + '../base-credentials-form/base-credentials-form.component.css', + './postgres-credentials-form.component.css', + ], + imports: [ + NgIf, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatCheckboxModule, + MatExpansionModule, + HostnameValidationDirective, + MasterEncryptionPasswordComponent, + Angulartics2Module, + ], }) export class PostgresCredentialsFormComponent extends BaseCredentialsFormComponent { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/redis-credentials-form/redis-credentials-form.component.html b/frontend/src/app/components/connect-db/db-credentials-forms/redis-credentials-form/redis-credentials-form.component.html index ad612ffa6..547e5d96c 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/redis-credentials-form/redis-credentials-form.component.html +++ b/frontend/src/app/components/connect-db/db-credentials-forms/redis-credentials-form/redis-credentials-form.component.html @@ -4,6 +4,7 @@ data-testid="connection-hostname-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: hostname is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: hostname is edited')" required hostnameValidator [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -28,6 +29,7 @@ data-testid="connection-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: port is edited')" required [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" @@ -41,6 +43,7 @@ data-testid="connection-password-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: password is edited" + (change)="posthog.capture('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" @@ -69,6 +72,7 @@ data-testid="connection-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: username is edited')" [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" [(ngModel)]="connection.username"> @@ -81,6 +85,7 @@ data-testid="connection-database-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: database name is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: database name is edited')" [readonly]="(accessLevel === 'readonly' || connection.isTestConnection) && connection.id" [disabled]="submitting" [(ngModel)]="connection.database"> @@ -92,6 +97,7 @@ labelPosition="after" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH is switched')" [angularticsProperties]="{'enable': connection.ssh}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssh"> @@ -105,6 +111,7 @@ data-testid="connection-ssh-key-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH key is edited" + (change)="posthog.capture('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" @@ -118,6 +125,7 @@ data-testid="connection-ssh-host-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH host is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH host is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshHost"> @@ -130,6 +138,7 @@ data-testid="connection-ssh-port-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH port is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH port is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshPort"> @@ -143,6 +152,7 @@ data-testid="connection-ssh-username-input" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSH username is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSH username is edited')" [required]="connection.ssh" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.sshUsername"> @@ -154,6 +164,7 @@ data-testid="connection-ssl-checkbox" angulartics2On="click" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL is switched" + (click)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL is switched')" [angularticsProperties]="{'enable': connection.ssl}" [disabled]="submitting || connection.isTestConnection" [(ngModel)]="connection.ssl"> @@ -166,6 +177,7 @@ data-testid="connection-ssl-certificate-textarea" angulartics2On="change" angularticsAction="Connection creds {{ connection.id ? 'edit' : 'add' }}: SSL certificate is edited" + (change)="posthog.capture('Connection creds {{ connection.id ? \'edit\' : \'add\' }}: SSL certificate is edited')" [readonly]="accessLevel === 'readonly' && connection.id" [disabled]="submitting" [(ngModel)]="connection.cert" diff --git a/frontend/src/app/components/connect-db/db-credentials-forms/redis-credentials-form/redis-credentials-form.component.ts b/frontend/src/app/components/connect-db/db-credentials-forms/redis-credentials-form/redis-credentials-form.component.ts index fff5c7836..6d24ee576 100644 --- a/frontend/src/app/components/connect-db/db-credentials-forms/redis-credentials-form/redis-credentials-form.component.ts +++ b/frontend/src/app/components/connect-db/db-credentials-forms/redis-credentials-form/redis-credentials-form.component.ts @@ -1,31 +1,32 @@ -import { Angulartics2Module } from 'angulartics2'; -import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; +import { NgIf } from '@angular/common'; 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'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { HostnameValidationDirective } from 'src/app/directives/hostnameValidator.directive'; +import { MasterEncryptionPasswordComponent } from '../../master-encryption-password/master-encryption-password.component'; +import { BaseCredentialsFormComponent } from '../base-credentials-form/base-credentials-form.component'; @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 - ] + 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 { - + protected posthog = posthog; } diff --git a/frontend/src/app/components/connect-db/master-encryption-password/master-encryption-password.component.html b/frontend/src/app/components/connect-db/master-encryption-password/master-encryption-password.component.html index 5eb83fb7e..65ae7d7ae 100644 --- a/frontend/src/app/components/connect-db/master-encryption-password/master-encryption-password.component.html +++ b/frontend/src/app/components/connect-db/master-encryption-password/master-encryption-password.component.html @@ -3,6 +3,7 @@ data-testid="connection-master-encryption-toggle" angulartics2On="click" angularticsAction="Connection creds: client-side encryption is switched" + (click)="posthog.capture('Connection creds: client-side encryption is switched')" [angularticsProperties]="{'enable': isMasterKeyTurnedOn}" [disabled]="disabled" [(ngModel)]="isMasterKeyTurnedOn" diff --git a/frontend/src/app/components/connect-db/master-encryption-password/master-encryption-password.component.ts b/frontend/src/app/components/connect-db/master-encryption-password/master-encryption-password.component.ts index 0c8bf1adc..7b76c8360 100644 --- a/frontend/src/app/components/connect-db/master-encryption-password/master-encryption-password.component.ts +++ b/frontend/src/app/components/connect-db/master-encryption-password/master-encryption-password.component.ts @@ -1,9 +1,6 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; - -import { Angulartics2Module } from 'angulartics2'; import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; -import { ConnectionsService } from 'src/app/services/connections.service'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -11,56 +8,60 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; +import { ConnectionsService } from 'src/app/services/connections.service'; import { NotificationsService } from 'src/app/services/notifications.service'; @Component({ - selector: 'app-master-encryption-password', - templateUrl: './master-encryption-password.component.html', - styleUrl: './master-encryption-password.component.css', - imports: [ - CommonModule, - FormsModule, - MatSlideToggleModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, - MatIconModule, - MatTooltipModule, - ClipboardModule, - Angulartics2Module - ] + selector: 'app-master-encryption-password', + templateUrl: './master-encryption-password.component.html', + styleUrl: './master-encryption-password.component.css', + imports: [ + CommonModule, + FormsModule, + MatSlideToggleModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + MatTooltipModule, + ClipboardModule, + Angulartics2Module, + ], }) export class MasterEncryptionPasswordComponent implements OnInit { - @Input() disabled: boolean; - @Input() masterKey: string; - // @Input() isMasterKeyTurnedOn: boolean; + protected posthog = posthog; + @Input() disabled: boolean; + @Input() masterKey: string; + // @Input() isMasterKeyTurnedOn: boolean; - @Output() onMasterKeyChange = new EventEmitter(); - // @Output() onMasterKeyToggle = new EventEmitter(); + @Output() onMasterKeyChange = new EventEmitter(); + // @Output() onMasterKeyToggle = new EventEmitter(); - public isMasterKeyTurnedOn: boolean = false; + public isMasterKeyTurnedOn: boolean = false; - constructor( - private _connections: ConnectionsService, - private _notifications: NotificationsService, - ) { } + constructor( + private _connections: ConnectionsService, + private _notifications: NotificationsService, + ) {} - ngOnInit() { - this.isMasterKeyTurnedOn = this._connections.currentConnection.masterEncryption; - } + ngOnInit() { + this.isMasterKeyTurnedOn = this._connections.currentConnection.masterEncryption; + } - generatePassword (checked: boolean) { - if (checked) { - let randomArray = new Uint8Array(32); - window.crypto.getRandomValues(randomArray); - this.masterKey = btoa(String.fromCharCode(...randomArray)); + generatePassword(checked: boolean) { + if (checked) { + let randomArray = new Uint8Array(32); + window.crypto.getRandomValues(randomArray); + this.masterKey = btoa(String.fromCharCode(...randomArray)); - this.onMasterKeyChange.emit(this.masterKey); - } - // this.onMasterKeyToggle.emit(checked); - } + this.onMasterKeyChange.emit(this.masterKey); + } + // this.onMasterKeyToggle.emit(checked); + } - showCopyNotification(message: string) { - this._notifications.showSuccessSnackbar(message); - } + showCopyNotification(message: string) { + this._notifications.showSuccessSnackbar(message); + } } diff --git a/frontend/src/app/components/connection-settings/connection-settings.component.html b/frontend/src/app/components/connection-settings/connection-settings.component.html index c8e11cf6e..b2b537dcb 100644 --- a/frontend/src/app/components/connection-settings/connection-settings.component.html +++ b/frontend/src/app/components/connection-settings/connection-settings.component.html @@ -50,6 +50,7 @@

Rocketadmin can not find any tables

Username should not be empty. @@ -61,6 +62,7 @@

Rocketadmin can not find any tables

Username should not be empty. @@ -74,6 +76,7 @@

Rocketadmin can not find any tables

Username should not be empty. @@ -87,6 +90,7 @@

Rocketadmin can not find any tables

Username should not be empty. @@ -102,6 +106,7 @@

Rocketadmin can not find any tables

[disabled]="submitting" angulartics2On="click" angularticsAction="Connection settings: hidden tables is selected" + (click)="posthog.capture('Connection settings: hidden tables is selected')" [(ngModel)]="connectionSettings.hidden_tables">
@@ -119,6 +124,7 @@

Rocketadmin can not find any tables

[disabled]="submitting" angulartics2On="click" angularticsAction="Connection settings: default display table is selected" + (click)="posthog.capture('Connection settings: default display table is selected')" [(ngModel)]="connectionSettings.default_showing_table"> @@ -135,6 +141,7 @@

Rocketadmin can not find any tables

[disabled]="submitting" angulartics2On="click" angularticsAction="Connection settings: log recording is toggled" + (click)="posthog.capture('Connection settings: log recording is toggled')" [angularticsProperties]="{'enable': connectionSettings.tables_audit}" [(ngModel)]="connectionSettings.tables_audit"> Log changes in tables diff --git a/frontend/src/app/components/connection-settings/connection-settings.component.ts b/frontend/src/app/components/connection-settings/connection-settings.component.ts index 2b93e1db8..6e8738359 100644 --- a/frontend/src/app/components/connection-settings/connection-settings.component.ts +++ b/frontend/src/app/components/connection-settings/connection-settings.component.ts @@ -10,6 +10,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { Title } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { Angulartics2, Angulartics2Module } from 'angulartics2'; +import posthog from 'posthog-js'; import { take } from 'rxjs'; import { ServerError } from 'src/app/models/alert'; import { ConnectionSettings } from 'src/app/models/connection'; @@ -45,6 +46,7 @@ import { BannerComponent } from '../ui-components/banner/banner.component'; ], }) export class ConnectionSettingsComponent implements OnInit { + protected posthog = posthog; public isSaas = (environment as any).saas; public connectionID: string | null = null; public tablesList: TableProperties[] = null; @@ -159,6 +161,7 @@ export class ConnectionSettingsComponent implements OnInit { action: 'Connection settings: settings is created successfully', properties: updatedSettings, }); + posthog.capture('Connection settings: settings is created successfully'); }, () => (this.submitting = false), () => (this.submitting = false), @@ -186,6 +189,7 @@ export class ConnectionSettingsComponent implements OnInit { action: 'Connection settings: settings is updated successfully', properties: updatedSettings, }); + posthog.capture('Connection settings: settings is updated successfully'); }, () => (this.submitting = false), () => (this.submitting = false), @@ -201,6 +205,7 @@ export class ConnectionSettingsComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Connection settings: settings is reset successfully', }); + posthog.capture('Connection settings: settings is reset successfully'); }, () => (this.submitting = false), () => (this.submitting = false), diff --git a/frontend/src/app/components/connections-list/demo-connections/demo-connections.component.html b/frontend/src/app/components/connections-list/demo-connections/demo-connections.component.html index dfe3867d4..38ccea385 100644 --- a/frontend/src/app/components/connections-list/demo-connections/demo-connections.component.html +++ b/frontend/src/app/components/connections-list/demo-connections/demo-connections.component.html @@ -7,6 +7,7 @@

Try out a demo admin panel

attr.data-testid="{{testConnectionItem.connection.id}}-test-connection-list-item" angulartics2On="click" angularticsAction="Connections: open test db is clicked" + (click)="posthog.capture('Connections: open test db is clicked')" [angularticsProperties]="{'dbType': testConnectionItem.connection.type}"> 0) { - this.testConnections = this.orderTestDatabases(); - } - } - - orderTestDatabases() { - if (!this.testConnections || !Array.isArray(this.testConnections) || this.testConnections.length === 0) { - return []; - } - - const orderMap = new Map(supportedOrderedDatabases.map((db, index) => [db, index])); - - return [...this.testConnections].sort((a, b) => { - const typeA = a.connection?.type; - const typeB = b.connection?.type; - - if (!typeA || !typeB) { - return 0; - } - - const indexA = orderMap.has(typeA) ? orderMap.get(typeA) : Infinity; - const indexB = orderMap.has(typeB) ? orderMap.get(typeB) : Infinity; - - return indexA - indexB; - }); -} + protected posthog = posthog; + @Input() testConnections: ConnectionItem[] = []; + @Input() isDemo: boolean = false; + + public testDatabasesNames = supportedDatabasesTitles; + + public testDatabasesIcons = { + mysql: '/assets/icons/test-connections-icons/diversity_2.svg', + postgres: '/assets/icons/test-connections-icons/set_meal.svg', + mongodb: '/assets/icons/test-connections-icons/cinematic_blur.svg', + dynamodb: '/assets/icons/test-connections-icons/box.svg', + oracledb: '/assets/icons/test-connections-icons/shopping_bag_speed.svg', + mssql: '/assets/icons/test-connections-icons/add_shopping_cart.svg', + }; + + ngOnInit() { + if (this.testConnections && this.testConnections.length > 0) { + this.testConnections = this.orderTestDatabases(); + } + } + + orderTestDatabases() { + if (!this.testConnections || !Array.isArray(this.testConnections) || this.testConnections.length === 0) { + return []; + } + + const orderMap = new Map(supportedOrderedDatabases.map((db, index) => [db, index])); + + return [...this.testConnections].sort((a, b) => { + const typeA = a.connection?.type; + const typeB = b.connection?.type; + + if (!typeA || !typeB) { + return 0; + } + + const indexA = orderMap.has(typeA) ? orderMap.get(typeA) : Infinity; + const indexB = orderMap.has(typeB) ? orderMap.get(typeB) : Infinity; + + return indexA - indexB; + }); + } } diff --git a/frontend/src/app/components/connections-list/own-connections/own-connections.component.html b/frontend/src/app/components/connections-list/own-connections/own-connections.component.html index 994b9c215..c9dc69add 100644 --- a/frontend/src/app/components/connections-list/own-connections/own-connections.component.html +++ b/frontend/src/app/components/connections-list/own-connections/own-connections.component.html @@ -23,7 +23,8 @@

+ angularticsAction="Connections: open own db is clicked" + (click)="posthog.capture('Connections: open own db is clicked')">
{{ connectionItem.displayTitle }} + angularticsAction="Connections: Add connection button is clicked" + (click)="posthog.capture('Connections: Add connection button is clicked')"> add Add database @@ -72,7 +74,8 @@

{{ connectionItem.displayTitle }} + angularticsAction="Connections: Add connection button is clicked" + (click)="posthog.capture('Connections: Add connection button is clicked')"> add Add database --> diff --git a/frontend/src/app/components/connections-list/own-connections/own-connections.component.ts b/frontend/src/app/components/connections-list/own-connections/own-connections.component.ts index d6f5ba87c..c28891e22 100644 --- a/frontend/src/app/components/connections-list/own-connections/own-connections.component.ts +++ b/frontend/src/app/components/connections-list/own-connections/own-connections.component.ts @@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { RouterModule } from '@angular/router'; import { User } from '@sentry/angular'; +import posthog from 'posthog-js'; import { supportedDatabasesTitles, supportedOrderedDatabases } from 'src/app/consts/databases'; import { ConnectionItem } from 'src/app/models/connection'; import { UiSettings } from 'src/app/models/ui-settings'; @@ -16,6 +17,7 @@ import { UiSettingsService } from 'src/app/services/ui-settings.service'; styleUrl: './own-connections.component.css', }) export class OwnConnectionsComponent { + protected posthog = posthog; @Input() currentUser: User; @Input() connections: ConnectionItem[] = null; @Input() isDemo: boolean = false; diff --git a/frontend/src/app/components/dashboard/dashboard.component.html b/frontend/src/app/components/dashboard/dashboard.component.html index d3e62a126..05765acc6 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.html +++ b/frontend/src/app/components/dashboard/dashboard.component.html @@ -34,7 +34,7 @@

Rocketadmin can not find any tables

-
@@ -195,7 +195,7 @@

Automations

diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.ts index 97a38fdf7..374ff7414 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-actions/db-table-actions.component.ts @@ -1,25 +1,11 @@ - -import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; -import { Component, OnInit } from '@angular/core'; -import { CustomAction, CustomActionMethod, CustomActionType, CustomEvent, EventType, Rule } from 'src/app/models/table'; - -import { ActionDeleteDialogComponent } from './action-delete-dialog/action-delete-dialog.component'; -import { AlertComponent } from '../../../ui-components/alert/alert.component'; -import { BreadcrumbsComponent } from '../../../ui-components/breadcrumbs/breadcrumbs.component'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { CodeEditorModule } from '@ngstack/code-editor'; import { CommonModule } from '@angular/common'; -import { CompanyMember } from 'src/app/models/company'; -import { CompanyService } from 'src/app/services/company.service'; -import { ConnectionsService } from 'src/app/services/connections.service'; -import { ContentLoaderComponent } from '../../../ui-components/content-loader/content-loader.component'; -import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; -import { IconPickerComponent } from '../../../ui-components/icon-picker/icon-picker.component'; +import { Component, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDialog } from '@angular/material/dialog'; -import { MatDialogModule } from '@angular/material/dialog'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatListModule } from '@angular/material/list'; @@ -28,377 +14,414 @@ import { MatSelectModule } from '@angular/material/select'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { Title } from '@angular/platform-browser'; +import { CodeEditorModule } from '@ngstack/code-editor'; +import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; +import posthog from 'posthog-js'; +import { codeSnippets } from 'src/app/consts/code-snippets'; +import { normalizeTableName } from 'src/app/lib/normalize'; +import { CompanyMember } from 'src/app/models/company'; +import { CustomAction, CustomActionMethod, CustomActionType, CustomEvent, EventType, Rule } from 'src/app/models/table'; +import { CompanyService } from 'src/app/services/company.service'; +import { ConnectionsService } from 'src/app/services/connections.service'; import { NotificationsService } from 'src/app/services/notifications.service'; import { TablesService } from 'src/app/services/tables.service'; -import { Title } from '@angular/platform-browser'; import { UiSettingsService } from 'src/app/services/ui-settings.service'; import { UserService } from 'src/app/services/user.service'; -import { codeSnippets } from 'src/app/consts/code-snippets'; -import { normalizeTableName } from 'src/app/lib/normalize'; +import { AlertComponent } from '../../../ui-components/alert/alert.component'; +import { BreadcrumbsComponent } from '../../../ui-components/breadcrumbs/breadcrumbs.component'; +import { ContentLoaderComponent } from '../../../ui-components/content-loader/content-loader.component'; +import { IconPickerComponent } from '../../../ui-components/icon-picker/icon-picker.component'; +import { ActionDeleteDialogComponent } from './action-delete-dialog/action-delete-dialog.component'; @Component({ - selector: 'app-db-table-actions', - templateUrl: './db-table-actions.component.html', - styleUrls: ['./db-table-actions.component.css'], - imports: [ - CommonModule, - ClipboardModule, - FormsModule, - MatButtonModule, - MatCheckboxModule, - MatDialogModule, - MatIconModule, - MatInputModule, - MatSelectModule, - MatTooltipModule, - MatSidenavModule, - MatListModule, - MatRadioModule, - MatTabsModule, - CodeEditorModule, - AlertComponent, - BreadcrumbsComponent, - ContentLoaderComponent, - IconPickerComponent, - Angulartics2OnModule - ] + selector: 'app-db-table-actions', + templateUrl: './db-table-actions.component.html', + styleUrls: ['./db-table-actions.component.css'], + imports: [ + CommonModule, + ClipboardModule, + FormsModule, + MatButtonModule, + MatCheckboxModule, + MatDialogModule, + MatIconModule, + MatInputModule, + MatSelectModule, + MatTooltipModule, + MatSidenavModule, + MatListModule, + MatRadioModule, + MatTabsModule, + CodeEditorModule, + AlertComponent, + BreadcrumbsComponent, + ContentLoaderComponent, + IconPickerComponent, + Angulartics2OnModule, + ], }) export class DbTableActionsComponent implements OnInit { - public connectionID: string | null = null; - public tableName: string | null = null; - public normalizedTableName: string; - public rulesData: { - table_name: string, - display_name: string, - action_rules: Rule[] - }; - public rules: Rule[]; - public submitting: boolean; - public selectedRule: Rule = null; - public selectedRuleCustomEvent: CustomEvent = null; - public customAction: CustomAction = null; - public selectedRuleTitle: string; - public newRule: Rule =null; - public newAction: CustomAction =null; - public actionNameError: string; - public codeSnippets: object; - public companyMembers: CompanyMember[]; - - public defaultIcons = ['favorite_outline', 'star_outline', 'done', 'arrow_forward', 'key_outline', 'lock', 'visibility', 'language', 'notifications', 'schedule']; - - public signingKey: string; - - public codeViewerOptions = { - minimap: { enabled: false }, - scrollBeyondLastLine: false, - wordWrap: 'on', - automaticLayout: true, - }; - - public availableEvents = [ - { value: EventType.AddRow, label: 'Add row' }, - { value: EventType.UpdateRow, label: 'Update row' }, - { value: EventType.DeleteRow, label: 'Delete row' }, - { value: EventType.Custom, label: 'Custom' } - ]; - public selectedEvents: string[] = []; - public codeEditorTheme: 'vs' | 'vs-dark' = 'vs-dark'; - - constructor( - private _connections: ConnectionsService, - private _tables: TablesService, - private _notifications: NotificationsService, - private _company: CompanyService, - private _userService: UserService, - private _uiSettings: UiSettingsService, - public dialog: MatDialog, - private title: Title, - private angulartics2: Angulartics2, - ) { } - - async ngOnInit() { - this.connectionID = this._connections.currentConnectionID; - this.tableName = this._tables.currentTableName; - this.normalizedTableName = normalizeTableName(this.tableName); - this._connections.getCurrentConnectionSigningKey().subscribe(signingKey => this.codeSnippets = codeSnippets(signingKey)); - this.codeEditorTheme = this._uiSettings.editorTheme; - - try { - this.rulesData = await this.getRules(); - console.log(this.rulesData); - this.rules = this.rulesData.action_rules; - if (this.rules.length) this.setSelectedRule(this.rules[0]); - this.title.setTitle(`${this.rulesData.display_name || this.normalizedTableName} - Actions | ${this._company.companyTabTitle || 'Rocketadmin'}`); - } catch(error) { - if (error instanceof HttpErrorResponse) { - this.rulesData = null; - console.log(error.error.message); - } else { throw error }; - } - - this._userService.cast - .subscribe(user => { - this._company.fetchCompanyMembers(user.company.id).subscribe(members => { - this.companyMembers = members; - }) - }); - - this._tables.cast.subscribe(async (arg) => { - if (arg === 'delete-rule') { - try { - this.rulesData = await this.getRules(); - this.rules = this.rulesData.action_rules; - if (this.rules.length) this.setSelectedRule(this.rules[0]); - } catch(error) { - if (error instanceof HttpErrorResponse) { - console.log(error.error.message); - } else { throw error }; - } - } - }); - } - - get currentConnection() { - // this.codeSnippets = codeSnippets(this._connections.currentConnection.signing_key); - return this._connections.currentConnection; - } - - // get codeSnippets() { - // return codeSnippets(this._connections.currentConnection.signing_key); - // } - - getCrumbs(name: string) { - return [ - { - label: name, - link: `/dashboard/${this.connectionID}` - }, - { - label: this.rulesData.display_name || this.normalizedTableName, - link: `/dashboard/${this.connectionID}/${this.tableName}` - }, - { - label: 'Automations', - link: null - } - ] - } - - trackByFn(index: number) { - return index; // or item.id - } - - setSelectedRule(rule: Rule) { - this.selectedRule = rule; - this.selectedRuleTitle = rule.title; - if (this.selectedRule.events[this.selectedRule.events.length - 1].event !== null) this.selectedRule.events.push({ event: null }); - this.selectedEvents = this.selectedRule.events.map((event) => event.event); - - const customEvent = this.selectedRule.events.find((event) => event.event === EventType.Custom); - if (customEvent) { - this.selectedRuleCustomEvent = customEvent as CustomEvent; - } else { - this.selectedRuleCustomEvent = null; - } - } - - updateIcon(icon: string) { - this.selectedRuleCustomEvent.icon = icon; - } - - switchRulesView(rule: Rule) { - this.setSelectedRule(rule); - } - - addNewRule() { - this.newRule = { - id: '', - title: '', - table_name: this.tableName, - events: [ - { - event: null - } - ], - table_actions: [ - { - method: CustomActionMethod.URL, - emails: [], - url: '', - } - ] - }; - - this.setSelectedRule(this.newRule); - } - - addNewAction() { - this.newAction = { - method: CustomActionMethod.URL, - emails: [], - url: '', - }; - } - - handleAddNewRule() { - this.actionNameError = null; - if (this.newRule.title === '') { - this.actionNameError = 'The name cannot be empty.'; - } else { - const coinsidingName = this.rules.find((rule: Rule) => rule.title === this.newRule.title); - if (!coinsidingName) { - this.selectedRule = {... this.newRule}; - this.selectedRuleTitle = this.selectedRule.title; - this.rules.push(this.selectedRule); - this.newRule = null; - } else { - this.actionNameError = 'You already have an action with this name.' - } - } - } - - undoRule() { - this.newRule = null; - if (this.rules.length) this.setSelectedRule(this.rules[0]); - } - - handleRemoveRule() { - if (this.selectedRule.id) { - this.openDeleteRuleDialog(); - } else { - this.removeRuleFromLocalList(this.selectedRule.title) - } - } - - removeRuleFromLocalList(ruleTitle: string) { - this.rules = this.rules.filter((rule: Rule) => rule.title !== ruleTitle); - if (this.rules.length) this.setSelectedRule(this.rules[0]); - } - - getRules() { - return this._tables.fetchRules(this.connectionID, this.tableName).toPromise(); - } - - handleRuleSubmitting() { - if (this.selectedRule.events.filter(event => event.event !== null).length > 0) { - if (this.selectedRule.id) { - this.updateRule(); - } else { - this.addRule(); - } - } - } - - addRule() { - this.submitting = true; - this.selectedRule.events = this.selectedRule.events.filter((event) => event.event !== null); - this.selectedRule.events = this.selectedRule.events.map(event => { - if (event.event === 'CUSTOM') { - return {...event, ...this.selectedRuleCustomEvent}; - } - return event; - }); - - this._tables.saveRule(this.connectionID, this.tableName, this.selectedRule) - .subscribe(async (res) => { - this.submitting = false; - this.angulartics2.eventTrack.next({ - action: 'Rules: rule is saved successfully' - }); - try { - const undatedRulesData = await this.getRules(); - this.rules = undatedRulesData.action_rules; - const currentRule = this.rules.find((rule: Rule) => rule.id === res.id); - this.setSelectedRule(currentRule); - } catch(error) { - if (error instanceof HttpErrorResponse) { - console.log(error.error.message); - } else { throw error }; - } - }, - () => this.submitting = false, - () => this.submitting = false - ) - } - - updateRule() { - this.submitting = true; - this.selectedRule.events = this.selectedRule.events.filter((event) => event.event !== null); - if (this.selectedRuleTitle) this.selectedRule.title = this.selectedRuleTitle; - this.selectedRule.events = this.selectedRule.events.map(event => { - if (event.event === 'CUSTOM') { - return {...event, ...this.selectedRuleCustomEvent}; - } - return event; - }); - - this._tables.updateRule(this.connectionID, this.tableName, this.selectedRule) - .subscribe(async (res) => { - this.submitting = false; - try { - const undatedRulesData = await this.getRules(); - this.rules = undatedRulesData.action_rules; - const currentRule = this.rules.find((rule: Rule) => rule.id === res.id); - this.setSelectedRule(currentRule); - } catch(error) { - if (error instanceof HttpErrorResponse) { - console.log(error.error.message); - } else { throw error }; - } - }, - () => this.submitting = false, - () => this.submitting = false - ) - } - - openDeleteRuleDialog() { - this.dialog.open(ActionDeleteDialogComponent, { - width: '25em', - data: { - connectionID: this.connectionID, - tableName: this.tableName, - rule: this.selectedRule - } - }) - } - - showCopyNotification(message: string) { - this._notifications.showSuccessSnackbar(message); - } - - onEventChange(event: any) { - console.log(this.selectedRule.events); - this.selectedEvents.push(event.value); - - let customEvent = this.selectedRule.events.find((event) => event.event === EventType.Custom); - - if (event.value === EventType.Custom) { - customEvent = { - ...customEvent, - title: '', - type: CustomActionType.Single, - icon: '', - require_confirmation: false - }; - // this.selectedRule.events.push(customEvent); - this.selectedRuleCustomEvent = customEvent; - } - - if (this.selectedRule.events.length < 4) { - this.selectedRule.events.push({ event: null }); - } - } - - removeEvent(event: any) { - this.selectedRule.events = this.selectedRule.events.filter((e) => e.event !== event); - this.selectedEvents = this.selectedRule.events.map((event) => event.event); - - if (event === EventType.Custom) { - this.selectedRuleCustomEvent = null; - } - - if (this.selectedRule.events.length === 3) { - this.selectedRule.events.push({ event: null }); - } - } + protected posthog = posthog; + public connectionID: string | null = null; + public tableName: string | null = null; + public normalizedTableName: string; + public rulesData: { + table_name: string; + display_name: string; + action_rules: Rule[]; + }; + public rules: Rule[]; + public submitting: boolean; + public selectedRule: Rule = null; + public selectedRuleCustomEvent: CustomEvent = null; + public customAction: CustomAction = null; + public selectedRuleTitle: string; + public newRule: Rule = null; + public newAction: CustomAction = null; + public actionNameError: string; + public codeSnippets: object; + public companyMembers: CompanyMember[]; + + public defaultIcons = [ + 'favorite_outline', + 'star_outline', + 'done', + 'arrow_forward', + 'key_outline', + 'lock', + 'visibility', + 'language', + 'notifications', + 'schedule', + ]; + + public signingKey: string; + + public codeViewerOptions = { + minimap: { enabled: false }, + scrollBeyondLastLine: false, + wordWrap: 'on', + automaticLayout: true, + }; + + public availableEvents = [ + { value: EventType.AddRow, label: 'Add row' }, + { value: EventType.UpdateRow, label: 'Update row' }, + { value: EventType.DeleteRow, label: 'Delete row' }, + { value: EventType.Custom, label: 'Custom' }, + ]; + public selectedEvents: string[] = []; + public codeEditorTheme: 'vs' | 'vs-dark' = 'vs-dark'; + + constructor( + private _connections: ConnectionsService, + private _tables: TablesService, + private _notifications: NotificationsService, + private _company: CompanyService, + private _userService: UserService, + private _uiSettings: UiSettingsService, + public dialog: MatDialog, + private title: Title, + private angulartics2: Angulartics2, + ) {} + + async ngOnInit() { + this.connectionID = this._connections.currentConnectionID; + this.tableName = this._tables.currentTableName; + this.normalizedTableName = normalizeTableName(this.tableName); + this._connections + .getCurrentConnectionSigningKey() + .subscribe((signingKey) => (this.codeSnippets = codeSnippets(signingKey))); + this.codeEditorTheme = this._uiSettings.editorTheme; + + try { + this.rulesData = await this.getRules(); + console.log(this.rulesData); + this.rules = this.rulesData.action_rules; + if (this.rules.length) this.setSelectedRule(this.rules[0]); + this.title.setTitle( + `${this.rulesData.display_name || this.normalizedTableName} - Actions | ${this._company.companyTabTitle || 'Rocketadmin'}`, + ); + } catch (error) { + if (error instanceof HttpErrorResponse) { + this.rulesData = null; + console.log(error.error.message); + } else { + throw error; + } + } + + this._userService.cast.subscribe((user) => { + this._company.fetchCompanyMembers(user.company.id).subscribe((members) => { + this.companyMembers = members; + }); + }); + + this._tables.cast.subscribe(async (arg) => { + if (arg === 'delete-rule') { + try { + this.rulesData = await this.getRules(); + this.rules = this.rulesData.action_rules; + if (this.rules.length) this.setSelectedRule(this.rules[0]); + } catch (error) { + if (error instanceof HttpErrorResponse) { + console.log(error.error.message); + } else { + throw error; + } + } + } + }); + } + + get currentConnection() { + // this.codeSnippets = codeSnippets(this._connections.currentConnection.signing_key); + return this._connections.currentConnection; + } + + // get codeSnippets() { + // return codeSnippets(this._connections.currentConnection.signing_key); + // } + + getCrumbs(name: string) { + return [ + { + label: name, + link: `/dashboard/${this.connectionID}`, + }, + { + label: this.rulesData.display_name || this.normalizedTableName, + link: `/dashboard/${this.connectionID}/${this.tableName}`, + }, + { + label: 'Automations', + link: null, + }, + ]; + } + + trackByFn(index: number) { + return index; // or item.id + } + + setSelectedRule(rule: Rule) { + this.selectedRule = rule; + this.selectedRuleTitle = rule.title; + if (this.selectedRule.events[this.selectedRule.events.length - 1].event !== null) + this.selectedRule.events.push({ event: null }); + this.selectedEvents = this.selectedRule.events.map((event) => event.event); + + const customEvent = this.selectedRule.events.find((event) => event.event === EventType.Custom); + if (customEvent) { + this.selectedRuleCustomEvent = customEvent as CustomEvent; + } else { + this.selectedRuleCustomEvent = null; + } + } + + updateIcon(icon: string) { + this.selectedRuleCustomEvent.icon = icon; + } + + switchRulesView(rule: Rule) { + this.setSelectedRule(rule); + } + + addNewRule() { + this.newRule = { + id: '', + title: '', + table_name: this.tableName, + events: [ + { + event: null, + }, + ], + table_actions: [ + { + method: CustomActionMethod.URL, + emails: [], + url: '', + }, + ], + }; + + this.setSelectedRule(this.newRule); + } + + addNewAction() { + this.newAction = { + method: CustomActionMethod.URL, + emails: [], + url: '', + }; + } + + handleAddNewRule() { + this.actionNameError = null; + if (this.newRule.title === '') { + this.actionNameError = 'The name cannot be empty.'; + } else { + const coinsidingName = this.rules.find((rule: Rule) => rule.title === this.newRule.title); + if (!coinsidingName) { + this.selectedRule = { ...this.newRule }; + this.selectedRuleTitle = this.selectedRule.title; + this.rules.push(this.selectedRule); + this.newRule = null; + } else { + this.actionNameError = 'You already have an action with this name.'; + } + } + } + + undoRule() { + this.newRule = null; + if (this.rules.length) this.setSelectedRule(this.rules[0]); + } + + handleRemoveRule() { + if (this.selectedRule.id) { + this.openDeleteRuleDialog(); + } else { + this.removeRuleFromLocalList(this.selectedRule.title); + } + } + + removeRuleFromLocalList(ruleTitle: string) { + this.rules = this.rules.filter((rule: Rule) => rule.title !== ruleTitle); + if (this.rules.length) this.setSelectedRule(this.rules[0]); + } + + getRules() { + return this._tables.fetchRules(this.connectionID, this.tableName).toPromise(); + } + + handleRuleSubmitting() { + if (this.selectedRule.events.filter((event) => event.event !== null).length > 0) { + if (this.selectedRule.id) { + this.updateRule(); + } else { + this.addRule(); + } + } + } + + addRule() { + this.submitting = true; + this.selectedRule.events = this.selectedRule.events.filter((event) => event.event !== null); + this.selectedRule.events = this.selectedRule.events.map((event) => { + if (event.event === 'CUSTOM') { + return { ...event, ...this.selectedRuleCustomEvent }; + } + return event; + }); + + this._tables.saveRule(this.connectionID, this.tableName, this.selectedRule).subscribe( + async (res) => { + this.submitting = false; + this.angulartics2.eventTrack.next({ + action: 'Rules: rule is saved successfully', + }); + posthog.capture('Rules: rule is saved successfully'); + try { + const undatedRulesData = await this.getRules(); + this.rules = undatedRulesData.action_rules; + const currentRule = this.rules.find((rule: Rule) => rule.id === res.id); + this.setSelectedRule(currentRule); + } catch (error) { + if (error instanceof HttpErrorResponse) { + console.log(error.error.message); + } else { + throw error; + } + } + }, + () => (this.submitting = false), + () => (this.submitting = false), + ); + } + + updateRule() { + this.submitting = true; + this.selectedRule.events = this.selectedRule.events.filter((event) => event.event !== null); + if (this.selectedRuleTitle) this.selectedRule.title = this.selectedRuleTitle; + this.selectedRule.events = this.selectedRule.events.map((event) => { + if (event.event === 'CUSTOM') { + return { ...event, ...this.selectedRuleCustomEvent }; + } + return event; + }); + + this._tables.updateRule(this.connectionID, this.tableName, this.selectedRule).subscribe( + async (res) => { + this.submitting = false; + try { + const undatedRulesData = await this.getRules(); + this.rules = undatedRulesData.action_rules; + const currentRule = this.rules.find((rule: Rule) => rule.id === res.id); + this.setSelectedRule(currentRule); + } catch (error) { + if (error instanceof HttpErrorResponse) { + console.log(error.error.message); + } else { + throw error; + } + } + }, + () => (this.submitting = false), + () => (this.submitting = false), + ); + } + + openDeleteRuleDialog() { + this.dialog.open(ActionDeleteDialogComponent, { + width: '25em', + data: { + connectionID: this.connectionID, + tableName: this.tableName, + rule: this.selectedRule, + }, + }); + } + + showCopyNotification(message: string) { + this._notifications.showSuccessSnackbar(message); + } + + onEventChange(event: any) { + console.log(this.selectedRule.events); + this.selectedEvents.push(event.value); + + let customEvent = this.selectedRule.events.find((event) => event.event === EventType.Custom); + + if (event.value === EventType.Custom) { + customEvent = { + ...customEvent, + title: '', + type: CustomActionType.Single, + icon: '', + require_confirmation: false, + }; + // this.selectedRule.events.push(customEvent); + this.selectedRuleCustomEvent = customEvent; + } + + if (this.selectedRule.events.length < 4) { + this.selectedRule.events.push({ event: null }); + } + } + + removeEvent(event: any) { + this.selectedRule.events = this.selectedRule.events.filter((e) => e.event !== event); + this.selectedEvents = this.selectedRule.events.map((event) => event.event); + + if (event === EventType.Custom) { + this.selectedRuleCustomEvent = null; + } + + if (this.selectedRule.events.length === 3) { + this.selectedRule.events.push({ event: null }); + } + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts index ba22afe16..0fc3e80bb 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts @@ -1,489 +1,508 @@ -import { Angulartics2, Angulartics2Module } from 'angulartics2'; -import { Component, ElementRef, HostListener, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { Subscription } from 'rxjs'; - import { CommonModule } from '@angular/common'; -import { ConnectionsService } from 'src/app/services/connections.service'; +import { Component, ElementRef, HostListener, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MarkdownModule } from 'ngx-markdown'; -import { MarkdownService } from 'ngx-markdown'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; +import { Angulartics2, Angulartics2Module } from 'angulartics2'; +import { MarkdownModule, MarkdownService } from 'ngx-markdown'; +import posthog from 'posthog-js'; +import { Subscription } from 'rxjs'; +import { ConnectionsService } from 'src/app/services/connections.service'; import { TableStateService } from 'src/app/services/table-state.service'; import { TablesService } from 'src/app/services/tables.service'; @Component({ - selector: 'app-db-table-ai-panel', - templateUrl: './db-table-ai-panel.component.html', - styleUrl: './db-table-ai-panel.component.css', - imports: [ - CommonModule, - FormsModule, - MarkdownModule, - MatIconModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, - Angulartics2Module - ] + selector: 'app-db-table-ai-panel', + templateUrl: './db-table-ai-panel.component.html', + styleUrl: './db-table-ai-panel.component.css', + imports: [ + CommonModule, + FormsModule, + MarkdownModule, + MatIconModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + Angulartics2Module, + ], }) export class DbTableAiPanelComponent implements OnInit, OnDestroy { - - @Input() public displayName: string; - @Input() public tableColumns: string[] = []; - @Input() public sidebarExpanded: boolean = true; - @ViewChild('chatContainer') private chatContainer!: ElementRef; - - public connectionID: string; - public tableName: string; - public isAIpanelOpened: boolean = false; - public message: string = ''; - public charactrsNumber: number = 0; - public threadID: string = null; - public messagesChain: { - type: string; - text: string - }[] = []; - public aiSuggestions: { title: string; prompt: string; completions: string[] }[] = []; - public suggestionCategories: { title: string; suggestions: { title: string; prompt: string; completions: string[] }[] }[] = []; - public activeSuggestion: { title: string; prompt: string; completions: string[] } | null = null; - public activeCompletions: string[] = []; - public showCompletions: boolean = false; - public submitting: boolean = false; - public isExpanded: boolean = false; - public textareaRows: number = 4; - public currentLoadingStep: string = ''; - - private _currentRequest: Subscription = null; - private _loadingStepsInterval: any = null; - private _loadingSteps: string[] = [ - 'Connecting to database', - 'Analyzing table structure', - 'Scanning records', - 'Processing your query', - 'Searching for patterns', - 'Generating response' - ]; - private _currentStepIndex: number = 0; - - constructor( - private _connections: ConnectionsService, - private _tables: TablesService, - private _tableState: TableStateService, - private markdownService: MarkdownService, - private angulartics2: Angulartics2, - ) { } - - ngOnInit(): void { - this.connectionID = this._connections.currentConnectionID; - this.tableName = this._tables.currentTableName; - - this._tableState.aiPanelCast.subscribe((isAIpanelOpened) => { - this.isAIpanelOpened = isAIpanelOpened; - }); - - this._tableState.aiPanelExpandedCast.subscribe((isExpanded) => { - this.isExpanded = isExpanded; - }); - - this.adjustTextareaRows(); - this.generateSuggestionCategories(); - } - - ngOnChanges(): void { - this.generateSuggestionCategories(); - } - - async ngAfterViewInit() { - const mermaid = await import("mermaid") - //@ts-expect-error dynamic load of mermaid - window.mermaid = mermaid.default ?? mermaid; - }; - - ngOnDestroy() { - this.cancelRequest(); - this.stopLoadingSteps(); - this.angulartics2.eventTrack.next({ - action: 'AI panel: destroyed', - properties: { - messagesLength: this.messagesChain.length - } - }); - } - - cancelRequest(): void { - if (this._currentRequest) { - this._currentRequest.unsubscribe(); - this._currentRequest = null; - this.submitting = false; - this.stopLoadingSteps(); - - this.messagesChain.push({ - type: 'ai-error', - text: 'Request cancelled' - }); - - this.angulartics2.eventTrack.next({ - action: 'AI panel: request cancelled', - }); - } - } - - startLoadingSteps(): void { - this._currentStepIndex = 0; - this.currentLoadingStep = this._loadingSteps[0]; - this._loadingStepsInterval = setInterval(() => { - this._currentStepIndex = (this._currentStepIndex + 1) % this._loadingSteps.length; - this.currentLoadingStep = this._loadingSteps[this._currentStepIndex]; - }, 2000); - } - - stopLoadingSteps(): void { - if (this._loadingStepsInterval) { - clearInterval(this._loadingStepsInterval); - this._loadingStepsInterval = null; - } - } - - onKeydown(event: KeyboardEvent): void { - this.charactrsNumber = this.message.length + 1; - - if (event.key === 'Enter') { - if (event.shiftKey) { - event.preventDefault(); - const textarea = event.target as HTMLTextAreaElement; - const cursorPos = textarea.selectionStart; - this.message = - this.message.substring(0, cursorPos) + '\n' + this.message.substring(cursorPos); - setTimeout(() => { - textarea.selectionStart = textarea.selectionEnd = cursorPos + 1; - }); - } else { - event.preventDefault(); - if (this.threadID) { - this.sendMessage(); - } else { - this.createThread(); - } - } - } - } - - createThread(suggestedMessage?: string) { - if (suggestedMessage) { - this.message = suggestedMessage; - } - this.submitting = true; - this.startLoadingSteps(); - this.messagesChain.push({ - type: 'user', - text: this.message - }); - const messageCopy = this.message; - this.message = ''; - - this._currentRequest = this._tables.createAIthread(this.connectionID, this.tableName, messageCopy).subscribe((response) => { - this.threadID = response.threadId; - - this.messagesChain.push({ - type: 'ai', - text: this.markdownService.parse(response.responseMessage) as string - }); - this.submitting = false; - this.stopLoadingSteps(); - this._currentRequest = null; - - this.angulartics2.eventTrack.next({ - action: 'AI panel: thread created successfully', - }); - }, - (error_message) => { - this.messagesChain.push({ - type: 'ai-error', - text: error_message - }); - this.angulartics2.eventTrack.next({ - action: 'AI panel: thread creation returned an error', - }); - this.stopLoadingSteps(); - this._currentRequest = null; - }, - () => { - this.submitting = false; - this.stopLoadingSteps(); - this._currentRequest = null; - } - ); - } - - sendMessage(suggestedMessage?: string): void { - if (suggestedMessage) { - this.message = suggestedMessage; - } - this.submitting = true; - this.startLoadingSteps(); - this.messagesChain.push({ - type: 'user', - text: this.message - }); - const messageCopy = this.message; - this.message = ''; - this.charactrsNumber = 0; - this._currentRequest = this._tables.requestAImessage(this.connectionID, this.tableName, this.threadID, messageCopy).subscribe((response_message) => { - this.messagesChain.push({ - type: 'ai', - text: this.markdownService.parse(response_message) as string - }); - this.submitting = false; - this.stopLoadingSteps(); - this._currentRequest = null; - - this.angulartics2.eventTrack.next({ - action: 'AI panel: message sent successfully', - }); - }, - (error_message) => { - this.messagesChain.push({ - type: 'ai-error', - text: error_message - }); - this.submitting = false; - this.stopLoadingSteps(); - this._currentRequest = null; - this.angulartics2.eventTrack.next({ - action: 'AI panel: message sent and returned an error', - }); - }, - () => { - this.submitting = false; - this.stopLoadingSteps(); - this._currentRequest = null; - }) - } - - scrollToBottom(): void { - if (this.chatContainer) this.chatContainer.nativeElement.scrollTop = this.chatContainer.nativeElement.scrollHeight; - } - - ngAfterViewChecked(): void { - this.scrollToBottom(); - } - - handleClose() { - this._tableState.handleViewAIpanel(); - } - - toggleExpand() { - this._tableState.toggleAIPanelExpanded(); - } - - @HostListener('window:resize') - onWindowResize() { - this.adjustTextareaRows(); - } - - @HostListener('document:click', ['$event']) - onDocumentClick(event: MouseEvent) { - if (!this.showCompletions) return; - - const target = event.target as HTMLElement; - const clickedOnChip = target.closest('.suggestion-chip'); - const clickedOnDropdown = target.closest('.ai-completions'); - const clickedOnInput = target.closest('.ai-welcome-form__field'); - - if (!clickedOnChip && !clickedOnDropdown && !clickedOnInput) { - this.showCompletions = false; - this.activeSuggestion = null; - } - } - - private adjustTextareaRows() { - const windowHeight = window.innerHeight; - - if (windowHeight < 500) { - this.textareaRows = 1; - } else if (windowHeight < 650) { - this.textareaRows = 2; - } else if (windowHeight < 800) { - this.textareaRows = 3; - } else if (windowHeight < 950) { - this.textareaRows = 4; - } else { - this.textareaRows = 5; - } - } - - private generateSuggestionCategories(): void { - const categories: { title: string; suggestions: { title: string; prompt: string; completions: string[] }[] }[] = []; - - // Category: Explore Data - categories.push({ - title: 'Explore', - suggestions: [ - { - title: 'Show recent records', - prompt: 'Show recent records', - completions: [ - 'Show recent records - the 10 most recently added', - 'Show recent records - from the last 24 hours', - 'Show recent records - the last entry added', - 'Show recent records - with newest timestamps' - ] - }, - { - title: 'Summarize table', - prompt: 'Summarize table', - completions: [ - 'Summarize table - structure and content overview', - 'Summarize table - what kind of data is stored', - 'Summarize table - purpose of each column', - 'Summarize table - record count and what they represent' - ] - }, - { - title: 'Find patterns', - prompt: 'Find patterns', - completions: [ - 'Find patterns - in this data', - 'Find patterns - correlations between columns', - 'Find patterns - common value combinations', - 'Find patterns - seasonal or cyclical trends' - ] - } - ] - }); - - // Category: Data Quality - categories.push({ - title: 'Data Quality', - suggestions: [ - { - title: 'Find issues', - prompt: 'Find issues', - completions: [ - 'Find issues - data quality problems', - 'Find issues - missing or NULL values', - 'Find issues - potential duplicates', - 'Find issues - inconsistent data formats' - ] - }, - { - title: 'Check duplicates', - prompt: 'Check duplicates', - completions: [ - 'Check duplicates - any duplicate records', - 'Check duplicates - rows that look similar', - 'Check duplicates - values appearing multiple times', - 'Check duplicates - records entered twice' - ] - }, - { - title: 'Validate data', - prompt: 'Validate data', - completions: [ - 'Validate data - required fields filled', - 'Validate data - incorrect values', - 'Validate data - records not matching patterns', - 'Validate data - outliers and unusual values' - ] - } - ] - }); - - // Category: Insights - categories.push({ - title: 'Insights', - suggestions: [ - { - title: 'Top values', - prompt: 'Top values', - completions: [ - 'Top values - most common in each column', - 'Top values - 10 most frequent entries', - 'Top values - categories with most records', - 'Top values - what appears most often' - ] - }, - { - title: 'Statistics', - prompt: 'Statistics', - completions: [ - 'Statistics - for numeric columns', - 'Statistics - min, max, and average', - 'Statistics - distribution of values', - 'Statistics - summary of this data' - ] - }, - { - title: 'Anomalies', - prompt: 'Anomalies', - completions: [ - 'Anomalies - unusual or unexpected values', - 'Anomalies - records that stand out', - 'Anomalies - statistical outliers', - 'Anomalies - what looks different or wrong' - ] - } - ] - }); - - this.suggestionCategories = categories; - } - - onSuggestionChipClick(suggestion: { title: string; prompt: string; completions: string[] }): void { - this.activeSuggestion = suggestion; - this.message = suggestion.prompt; - this.activeCompletions = suggestion.completions; - this.showCompletions = true; - - this.angulartics2.eventTrack.next({ - action: 'AI panel: suggestion chip clicked', - properties: { - suggestionTitle: suggestion.title - } - }); - } - - selectCompletion(completion: string): void { - this.message = completion; - this.showCompletions = false; - this.activeSuggestion = null; - this.createThread(completion); - - this.angulartics2.eventTrack.next({ - action: 'AI panel: completion selected', - properties: { - completion: completion - } - }); - } - - onWelcomeInputChange(): void { - if (this.message.length > 0 && (!this.activeSuggestion || this.message !== this.activeSuggestion.prompt)) { - this.showCompletions = false; - this.activeSuggestion = null; - } - } - - onCompletionHover(completion: string): void { - this.message = completion; - } - - onCompletionMouseLeave(): void { - if (this.activeSuggestion) { - this.message = this.activeSuggestion.prompt; - } - } - - onWelcomeInputFocus(): void { - if (this.activeSuggestion) { - this.showCompletions = true; - } - } - - onWelcomeInputBlur(): void { - // Dropdown closing is now handled by document click listener - } + @Input() public displayName: string; + @Input() public tableColumns: string[] = []; + @Input() public sidebarExpanded: boolean = true; + @ViewChild('chatContainer') private chatContainer!: ElementRef; + + public connectionID: string; + public tableName: string; + public isAIpanelOpened: boolean = false; + public message: string = ''; + public charactrsNumber: number = 0; + public threadID: string = null; + public messagesChain: { + type: string; + text: string; + }[] = []; + public aiSuggestions: { title: string; prompt: string; completions: string[] }[] = []; + public suggestionCategories: { + title: string; + suggestions: { title: string; prompt: string; completions: string[] }[]; + }[] = []; + public activeSuggestion: { title: string; prompt: string; completions: string[] } | null = null; + public activeCompletions: string[] = []; + public showCompletions: boolean = false; + public submitting: boolean = false; + public isExpanded: boolean = false; + public textareaRows: number = 4; + public currentLoadingStep: string = ''; + + private _currentRequest: Subscription = null; + private _loadingStepsInterval: any = null; + private _loadingSteps: string[] = [ + 'Connecting to database', + 'Analyzing table structure', + 'Scanning records', + 'Processing your query', + 'Searching for patterns', + 'Generating response', + ]; + private _currentStepIndex: number = 0; + + constructor( + private _connections: ConnectionsService, + private _tables: TablesService, + private _tableState: TableStateService, + private markdownService: MarkdownService, + private angulartics2: Angulartics2, + ) {} + + ngOnInit(): void { + this.connectionID = this._connections.currentConnectionID; + this.tableName = this._tables.currentTableName; + + this._tableState.aiPanelCast.subscribe((isAIpanelOpened) => { + this.isAIpanelOpened = isAIpanelOpened; + }); + + this._tableState.aiPanelExpandedCast.subscribe((isExpanded) => { + this.isExpanded = isExpanded; + }); + + this.adjustTextareaRows(); + this.generateSuggestionCategories(); + } + + ngOnChanges(): void { + this.generateSuggestionCategories(); + } + + async ngAfterViewInit() { + const mermaid = await import('mermaid'); + //@ts-expect-error dynamic load of mermaid + window.mermaid = mermaid.default ?? mermaid; + } + + ngOnDestroy() { + this.cancelRequest(); + this.stopLoadingSteps(); + this.angulartics2.eventTrack.next({ + action: 'AI panel: destroyed', + properties: { + messagesLength: this.messagesChain.length, + }, + }); + posthog.capture('AI panel: destroyed', { + messagesLength: this.messagesChain.length, + }); + } + + cancelRequest(): void { + if (this._currentRequest) { + this._currentRequest.unsubscribe(); + this._currentRequest = null; + this.submitting = false; + this.stopLoadingSteps(); + + this.messagesChain.push({ + type: 'ai-error', + text: 'Request cancelled', + }); + + this.angulartics2.eventTrack.next({ + action: 'AI panel: request cancelled', + }); + posthog.capture('AI panel: request cancelled'); + } + } + + startLoadingSteps(): void { + this._currentStepIndex = 0; + this.currentLoadingStep = this._loadingSteps[0]; + this._loadingStepsInterval = setInterval(() => { + this._currentStepIndex = (this._currentStepIndex + 1) % this._loadingSteps.length; + this.currentLoadingStep = this._loadingSteps[this._currentStepIndex]; + }, 2000); + } + + stopLoadingSteps(): void { + if (this._loadingStepsInterval) { + clearInterval(this._loadingStepsInterval); + this._loadingStepsInterval = null; + } + } + + onKeydown(event: KeyboardEvent): void { + this.charactrsNumber = this.message.length + 1; + + if (event.key === 'Enter') { + if (event.shiftKey) { + event.preventDefault(); + const textarea = event.target as HTMLTextAreaElement; + const cursorPos = textarea.selectionStart; + this.message = this.message.substring(0, cursorPos) + '\n' + this.message.substring(cursorPos); + setTimeout(() => { + textarea.selectionStart = textarea.selectionEnd = cursorPos + 1; + }); + } else { + event.preventDefault(); + if (this.threadID) { + this.sendMessage(); + } else { + this.createThread(); + } + } + } + } + + createThread(suggestedMessage?: string) { + if (suggestedMessage) { + this.message = suggestedMessage; + } + this.submitting = true; + this.startLoadingSteps(); + this.messagesChain.push({ + type: 'user', + text: this.message, + }); + const messageCopy = this.message; + this.message = ''; + + this._currentRequest = this._tables.createAIthread(this.connectionID, this.tableName, messageCopy).subscribe( + (response) => { + this.threadID = response.threadId; + + this.messagesChain.push({ + type: 'ai', + text: this.markdownService.parse(response.responseMessage) as string, + }); + this.submitting = false; + this.stopLoadingSteps(); + this._currentRequest = null; + + this.angulartics2.eventTrack.next({ + action: 'AI panel: thread created successfully', + }); + posthog.capture('AI panel: thread created successfully'); + }, + (error_message) => { + this.messagesChain.push({ + type: 'ai-error', + text: error_message, + }); + this.angulartics2.eventTrack.next({ + action: 'AI panel: thread creation returned an error', + }); + posthog.capture('AI panel: thread creation returned an error'); + this.stopLoadingSteps(); + this._currentRequest = null; + }, + () => { + this.submitting = false; + this.stopLoadingSteps(); + this._currentRequest = null; + }, + ); + } + + sendMessage(suggestedMessage?: string): void { + if (suggestedMessage) { + this.message = suggestedMessage; + } + this.submitting = true; + this.startLoadingSteps(); + this.messagesChain.push({ + type: 'user', + text: this.message, + }); + const messageCopy = this.message; + this.message = ''; + this.charactrsNumber = 0; + this._currentRequest = this._tables + .requestAImessage(this.connectionID, this.tableName, this.threadID, messageCopy) + .subscribe( + (response_message) => { + this.messagesChain.push({ + type: 'ai', + text: this.markdownService.parse(response_message) as string, + }); + this.submitting = false; + this.stopLoadingSteps(); + this._currentRequest = null; + + this.angulartics2.eventTrack.next({ + action: 'AI panel: message sent successfully', + }); + posthog.capture('AI panel: message sent successfully'); + }, + (error_message) => { + this.messagesChain.push({ + type: 'ai-error', + text: error_message, + }); + this.submitting = false; + this.stopLoadingSteps(); + this._currentRequest = null; + this.angulartics2.eventTrack.next({ + action: 'AI panel: message sent and returned an error', + }); + posthog.capture('AI panel: message sent and returned an error'); + }, + () => { + this.submitting = false; + this.stopLoadingSteps(); + this._currentRequest = null; + }, + ); + } + + scrollToBottom(): void { + if (this.chatContainer) this.chatContainer.nativeElement.scrollTop = this.chatContainer.nativeElement.scrollHeight; + } + + ngAfterViewChecked(): void { + this.scrollToBottom(); + } + + handleClose() { + this._tableState.handleViewAIpanel(); + } + + toggleExpand() { + this._tableState.toggleAIPanelExpanded(); + } + + @HostListener('window:resize') + onWindowResize() { + this.adjustTextareaRows(); + } + + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent) { + if (!this.showCompletions) return; + + const target = event.target as HTMLElement; + const clickedOnChip = target.closest('.suggestion-chip'); + const clickedOnDropdown = target.closest('.ai-completions'); + const clickedOnInput = target.closest('.ai-welcome-form__field'); + + if (!clickedOnChip && !clickedOnDropdown && !clickedOnInput) { + this.showCompletions = false; + this.activeSuggestion = null; + } + } + + private adjustTextareaRows() { + const windowHeight = window.innerHeight; + + if (windowHeight < 500) { + this.textareaRows = 1; + } else if (windowHeight < 650) { + this.textareaRows = 2; + } else if (windowHeight < 800) { + this.textareaRows = 3; + } else if (windowHeight < 950) { + this.textareaRows = 4; + } else { + this.textareaRows = 5; + } + } + + private generateSuggestionCategories(): void { + const categories: { title: string; suggestions: { title: string; prompt: string; completions: string[] }[] }[] = []; + + // Category: Explore Data + categories.push({ + title: 'Explore', + suggestions: [ + { + title: 'Show recent records', + prompt: 'Show recent records', + completions: [ + 'Show recent records - the 10 most recently added', + 'Show recent records - from the last 24 hours', + 'Show recent records - the last entry added', + 'Show recent records - with newest timestamps', + ], + }, + { + title: 'Summarize table', + prompt: 'Summarize table', + completions: [ + 'Summarize table - structure and content overview', + 'Summarize table - what kind of data is stored', + 'Summarize table - purpose of each column', + 'Summarize table - record count and what they represent', + ], + }, + { + title: 'Find patterns', + prompt: 'Find patterns', + completions: [ + 'Find patterns - in this data', + 'Find patterns - correlations between columns', + 'Find patterns - common value combinations', + 'Find patterns - seasonal or cyclical trends', + ], + }, + ], + }); + + // Category: Data Quality + categories.push({ + title: 'Data Quality', + suggestions: [ + { + title: 'Find issues', + prompt: 'Find issues', + completions: [ + 'Find issues - data quality problems', + 'Find issues - missing or NULL values', + 'Find issues - potential duplicates', + 'Find issues - inconsistent data formats', + ], + }, + { + title: 'Check duplicates', + prompt: 'Check duplicates', + completions: [ + 'Check duplicates - any duplicate records', + 'Check duplicates - rows that look similar', + 'Check duplicates - values appearing multiple times', + 'Check duplicates - records entered twice', + ], + }, + { + title: 'Validate data', + prompt: 'Validate data', + completions: [ + 'Validate data - required fields filled', + 'Validate data - incorrect values', + 'Validate data - records not matching patterns', + 'Validate data - outliers and unusual values', + ], + }, + ], + }); + + // Category: Insights + categories.push({ + title: 'Insights', + suggestions: [ + { + title: 'Top values', + prompt: 'Top values', + completions: [ + 'Top values - most common in each column', + 'Top values - 10 most frequent entries', + 'Top values - categories with most records', + 'Top values - what appears most often', + ], + }, + { + title: 'Statistics', + prompt: 'Statistics', + completions: [ + 'Statistics - for numeric columns', + 'Statistics - min, max, and average', + 'Statistics - distribution of values', + 'Statistics - summary of this data', + ], + }, + { + title: 'Anomalies', + prompt: 'Anomalies', + completions: [ + 'Anomalies - unusual or unexpected values', + 'Anomalies - records that stand out', + 'Anomalies - statistical outliers', + 'Anomalies - what looks different or wrong', + ], + }, + ], + }); + + this.suggestionCategories = categories; + } + + onSuggestionChipClick(suggestion: { title: string; prompt: string; completions: string[] }): void { + this.activeSuggestion = suggestion; + this.message = suggestion.prompt; + this.activeCompletions = suggestion.completions; + this.showCompletions = true; + + this.angulartics2.eventTrack.next({ + action: 'AI panel: suggestion chip clicked', + properties: { + suggestionTitle: suggestion.title, + }, + }); + posthog.capture('AI panel: suggestion chip clicked', { + suggestionTitle: suggestion.title, + }); + } + + selectCompletion(completion: string): void { + this.message = completion; + this.showCompletions = false; + this.activeSuggestion = null; + this.createThread(completion); + + this.angulartics2.eventTrack.next({ + action: 'AI panel: completion selected', + properties: { + completion: completion, + }, + }); + posthog.capture('AI panel: completion selected', { + completion: completion, + }); + } + + onWelcomeInputChange(): void { + if (this.message.length > 0 && (!this.activeSuggestion || this.message !== this.activeSuggestion.prompt)) { + this.showCompletions = false; + this.activeSuggestion = null; + } + } + + onCompletionHover(completion: string): void { + this.message = completion; + } + + onCompletionMouseLeave(): void { + if (this.activeSuggestion) { + this.message = this.activeSuggestion.prompt; + } + } + + onWelcomeInputFocus(): void { + if (this.activeSuggestion) { + this.showCompletions = true; + } + } + + onWelcomeInputBlur(): void { + // Dropdown closing is now handled by document click listener + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.ts index dfb4e64bf..882d553eb 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-export-dialog/db-table-export-dialog.component.ts @@ -1,71 +1,69 @@ -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Angulartics2 } from 'angulartics2'; -import { TablesService } from 'src/app/services/tables.service'; import { CommonModule } from '@angular/common'; +import { Component, Inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatDialogModule } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; +import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; +import { TablesService } from 'src/app/services/tables.service'; @Component({ - selector: 'app-db-table-export-dialog', - templateUrl: './db-table-export-dialog.component.html', - styleUrls: ['./db-table-export-dialog.component.css'], - imports: [ - CommonModule, - FormsModule, - MatButtonModule, - MatDialogModule, - MatInputModule, - MatRadioModule - ] + selector: 'app-db-table-export-dialog', + templateUrl: './db-table-export-dialog.component.html', + styleUrls: ['./db-table-export-dialog.component.css'], + imports: [CommonModule, FormsModule, MatButtonModule, MatDialogModule, MatInputModule, MatRadioModule], }) export class DbTableExportDialogComponent { + public recordsNumber: number = 10; + public recordsExportType: string = 'export-all'; + public submitting: boolean = false; - public recordsNumber: number = 10; - public recordsExportType: string = 'export-all'; - public submitting: boolean = false; - - constructor( - @Inject(MAT_DIALOG_DATA) public data: any, - private _tables: TablesService, - public dialogRef: MatDialogRef, - private angulartics2: Angulartics2, - ) { } - - exportCSV() { - this.submitting = true; - if (this.recordsExportType === 'export-all') { - this.data = { ...this.data, chunkSize: 100000 }; - }; + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + private _tables: TablesService, + public dialogRef: MatDialogRef, + private angulartics2: Angulartics2, + ) {} - if (this.recordsExportType === 'export-records-number') { - this.data = { ...this.data, chunkSize: this.recordsNumber }; - } + exportCSV() { + this.submitting = true; + if (this.recordsExportType === 'export-all') { + this.data = { ...this.data, chunkSize: 100000 }; + } - this._tables.exportTableCSV(this.data).subscribe(res => { - this.downloadCSV(res); - this.submitting = false; - this.dialogRef.close(); - this.angulartics2.eventTrack.next({ - action: 'Dashboard: db export is successful', - }); - }, - _err => { this.submitting = false; }, - () => { this.submitting = false; }); - } + if (this.recordsExportType === 'export-records-number') { + this.data = { ...this.data, chunkSize: this.recordsNumber }; + } - downloadCSV(data) { - const blob = new Blob([data], { type: 'text/csv' }); - const link = document.createElement('a'); - link.href = URL.createObjectURL(blob); - link.download = 'database.csv'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(link.href); - } + this._tables.exportTableCSV(this.data).subscribe( + (res) => { + this.downloadCSV(res); + this.submitting = false; + this.dialogRef.close(); + this.angulartics2.eventTrack.next({ + action: 'Dashboard: db export is successful', + }); + posthog.capture('Dashboard: db export is successful'); + }, + (_err) => { + this.submitting = false; + }, + () => { + this.submitting = false; + }, + ); + } + downloadCSV(data) { + const blob = new Blob([data], { type: 'text/csv' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = 'database.csv'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(link.href); + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html index dd2e0e29b..3f2b51b2f 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html @@ -147,14 +147,16 @@

diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts index 8c922838b..ac9c97da7 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.ts @@ -14,6 +14,7 @@ import { Angulartics2OnModule } from 'angulartics2'; import JSON5 from 'json5'; import { DynamicModule } from 'ng-dynamic-component'; import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io'; +import posthog from 'posthog-js'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { filterTypes } from 'src/app/consts/filter-types'; @@ -48,6 +49,7 @@ import { ContentLoaderComponent } from '../../../ui-components/content-loader/co ], }) export class DbTableFiltersDialogComponent implements OnInit, AfterViewInit { + protected posthog = posthog; public tableFilters = []; public fieldSearchControl = new FormControl(''); diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.ts index 6b3386a14..1567d1f68 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-import-dialog/db-table-import-dialog.component.ts @@ -1,50 +1,50 @@ -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Angulartics2 } from 'angulartics2'; -import { TablesService } from 'src/app/services/tables.service'; import { CommonModule } from '@angular/common'; +import { Component, Inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatDialogModule } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; +import { TablesService } from 'src/app/services/tables.service'; @Component({ - selector: 'app-db-table-import-dialog', - templateUrl: './db-table-import-dialog.component.html', - styleUrls: ['./db-table-import-dialog.component.css'], - imports: [ - CommonModule, - FormsModule, - MatButtonModule, - MatDialogModule - ] + selector: 'app-db-table-import-dialog', + templateUrl: './db-table-import-dialog.component.html', + styleUrls: ['./db-table-import-dialog.component.css'], + imports: [CommonModule, FormsModule, MatButtonModule, MatDialogModule], }) export class DbTableImportDialogComponent { + public file: File; + public submitting: boolean = false; - public file: File; - public submitting: boolean = false; - - constructor( - @Inject(MAT_DIALOG_DATA) public data: any, - private _tables: TablesService, - public dialogRef: MatDialogRef, - private angulartics2: Angulartics2, - ) { } + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + private _tables: TablesService, + public dialogRef: MatDialogRef, + private angulartics2: Angulartics2, + ) {} - onFileSelected(event: any) { - this.file = event.target.files[0]; - } + onFileSelected(event: any) { + this.file = event.target.files[0]; + } - importCSV() { - this.submitting = true; - this._tables.importTableCSV(this.data.connectionID, this.data.tableName, this.file).subscribe(_res => { - this.dialogRef.close(); - this.submitting = false; - this.angulartics2.eventTrack.next({ - action: 'Dashboard: db import is successful', - }); - }, - _err => {this.submitting = false; }, - () => { this.submitting = false; } - ); - } + importCSV() { + this.submitting = true; + this._tables.importTableCSV(this.data.connectionID, this.data.tableName, this.file).subscribe( + (_res) => { + this.dialogRef.close(); + this.submitting = false; + this.angulartics2.eventTrack.next({ + action: 'Dashboard: db import is successful', + }); + posthog.capture('Dashboard: db import is successful'); + }, + (_err) => { + this.submitting = false; + }, + () => { + this.submitting = false; + }, + ); + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts index 9cc36b4e2..c3425c796 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.ts @@ -1,16 +1,7 @@ -import { Component, OnInit } from '@angular/core'; -import { TableField, TableOrdering, TableSettings } from 'src/app/models/table'; - -import { AlertComponent } from '../../../ui-components/alert/alert.component'; -import { Angulartics2 } from 'angulartics2'; -import { BreadcrumbsComponent } from '../../../ui-components/breadcrumbs/breadcrumbs.component'; -import { CommonModule } from '@angular/common'; -import { CompanyService } from 'src/app/services/company.service'; -import { ConnectionsService } from 'src/app/services/connections.service'; import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop'; -import { FormsModule } from '@angular/forms'; -import { IconPickerComponent } from '../../../ui-components/icon-picker/icon-picker.component'; -import { Location } from '@angular/common'; +import { CommonModule, Location } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { FormsModule, NgForm } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -19,206 +10,233 @@ import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { MatSelectModule } from '@angular/material/select'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { NgForm } from '@angular/forms'; -import { PlaceholderTableSettingsComponent } from '../../../skeletons/placeholder-table-settings/placeholder-table-settings.component'; -import { Router } from '@angular/router'; -import { RouterModule } from '@angular/router'; -import { TablesService } from 'src/app/services/tables.service'; import { Title } from '@angular/platform-browser'; +import { Router, RouterModule } from '@angular/router'; +import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { normalizeTableName } from 'src/app/lib/normalize'; +import { TableField, TableOrdering, TableSettings } from 'src/app/models/table'; +import { CompanyService } from 'src/app/services/company.service'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { TablesService } from 'src/app/services/tables.service'; +import { PlaceholderTableSettingsComponent } from '../../../skeletons/placeholder-table-settings/placeholder-table-settings.component'; +import { AlertComponent } from '../../../ui-components/alert/alert.component'; +import { BreadcrumbsComponent } from '../../../ui-components/breadcrumbs/breadcrumbs.component'; +import { IconPickerComponent } from '../../../ui-components/icon-picker/icon-picker.component'; @Component({ - selector: 'app-db-table-settings', - templateUrl: './db-table-settings.component.html', - styleUrls: ['./db-table-settings.component.css'], - imports: [ - CommonModule, - FormsModule, - RouterModule, - DragDropModule, - MatButtonModule, - MatSlideToggleModule, - MatFormFieldModule, - MatInputModule, - MatSelectModule, - MatIconModule, - MatExpansionModule, - MatRadioModule, - AlertComponent, - BreadcrumbsComponent, - IconPickerComponent, - PlaceholderTableSettingsComponent - ] + selector: 'app-db-table-settings', + templateUrl: './db-table-settings.component.html', + styleUrls: ['./db-table-settings.component.css'], + imports: [ + CommonModule, + FormsModule, + RouterModule, + DragDropModule, + MatButtonModule, + MatSlideToggleModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatIconModule, + MatExpansionModule, + MatRadioModule, + AlertComponent, + BreadcrumbsComponent, + IconPickerComponent, + PlaceholderTableSettingsComponent, + ], }) export class DbTableSettingsComponent implements OnInit { - public connectionID: string | null = null; - public tableName: string | null = null; - public displayTableName: string | null = null; - public submitting: boolean = false; - public isSettingsExist: boolean = false; - public loading: boolean = true; - public fields: string[]; - public fields_to_exclude: string[]; - public orderChanged: boolean = false; - public iconChanged: boolean = false; - public listFieldsOrder: string[]; - public tableSettingsInitial: TableSettings = { - connection_id: '', - table_name: '', - icon: '', - display_name: '', - autocomplete_columns: [], - identity_column: '', - ordering: TableOrdering.Ascending, - ordering_field: '', - list_fields: [], - columns_view: [], - search_fields: [], - excluded_fields: [], - readonly_fields: [], - sortable_by: [], - sensitive_fields: [], - allow_csv_export: true, - allow_csv_import: true, - can_delete: true, - } - public tableSettings: TableSettings = null; - public defaultIcons = ['favorite', 'star', 'done', 'arrow_forward', 'key', 'lock', 'visibility', 'language', 'notifications', 'schedule']; - - - constructor( - private _tables: TablesService, - private _connections: ConnectionsService, - private _location: Location, - private _company: CompanyService, - public router: Router, - private title: Title, - private angulartics2: Angulartics2, - ) { } - - ngOnInit(): void { - this.connectionID = this._connections.currentConnectionID; - this.tableName = this._tables.currentTableName; - this.displayTableName = normalizeTableName(this.tableName); - this._tables.cast.subscribe(); - this._tables.fetchTableStructure(this.connectionID, this.tableName) - .subscribe(res => { - const primaryKeys = res.primaryColumns.map(primaryColumn => primaryColumn.column_name); - this.fields = res.structure.map((field: TableField) => field.column_name); - this.fields_to_exclude = this.fields.filter((field) => !primaryKeys.includes(field)); - this.getTableSettings(); - }); - } - - get currentConnection() { - return this._connections.currentConnection; - } - - getCrumbs(name: string) { - return [ - { - label: name, - link: `/dashboard/${this.connectionID}` - }, - { - label: this.tableSettings.display_name || this.displayTableName, - link: `/dashboard/${this.connectionID}/${this.tableName}` - }, - { - label: 'Settings', - link: null - } - ] - } - - goBack() { - this._location.back(); - } - - getTableSettings() { - this._tables.fetchTableSettings(this.connectionID, this.tableName) - .subscribe(res => { - this.loading = false; - if (Object.keys(res).length !== 0) { - this.isSettingsExist = true - this.tableSettings = res; - this.listFieldsOrder = [...res.list_fields]; - } else { - this.tableSettings = this.tableSettingsInitial; - }; - if (Object.keys(res).length === 0 || (res && res.list_fields && !res.list_fields.length)) { - this.listFieldsOrder = [...this.fields]; - }; - this.title.setTitle(`${res.display_name || this.displayTableName} - Table settings | ${this._company.companyTabTitle || 'Rocketadmin'}`); - } - ); - } - - updateIcon(icon: string) { - this.tableSettings.icon = icon; - this.iconChanged = true; - } - - drop(event: CdkDragDrop) { - moveItemInArray(this.listFieldsOrder, event.previousIndex, event.currentIndex); - this.tableSettings.list_fields = [...this.listFieldsOrder]; - this.orderChanged = true; - } - - resetColumnsOrder() { - this.tableSettings.list_fields = []; - this.listFieldsOrder = [...this.fields]; - this.orderChanged = true; - } - - updateSettings() { - this.submitting = true; - this.tableSettings.connection_id = this.connectionID; - this.tableSettings.table_name = this.tableName; - - const updatedSettings = {} - - for (const [key, value] of Object.entries(this.tableSettings)) { - if (key !== 'connection_id' && key !== 'table_name' && key !== 'ordering') { - if (Array.isArray(value)) { - if (key === 'list_fields') { - updatedSettings[key] = this.orderChanged; - } else { - updatedSettings[key] = value.length > 0; - } - } else { - updatedSettings[key] = Boolean(value); - } - } - } - - this._tables.updateTableSettings(this.isSettingsExist, this.connectionID, this.tableName, this.tableSettings) - .subscribe(() => { - this.submitting = false; - this.angulartics2.eventTrack.next({ - action: 'Table settings: updated successfully', - properties: updatedSettings - }); - this.router.navigate([`/dashboard/${this.connectionID}/${this.tableName}`]); - }, - () => { this.submitting = false; }, - () => { this.submitting = false; } - ); - } - - resetSettings(form: NgForm) { - this.submitting = true; - this._tables.deleteTableSettings(this.connectionID, this.tableName) - .subscribe(() => { - form.reset(); - this.submitting = false; - this.tableSettings = {...this.tableSettingsInitial}; - this.angulartics2.eventTrack.next({ - action: 'Table settings: reset successfully', - }); - }, - () => { this.submitting = false; }, - () => { this.submitting = false; } - ) - } + public connectionID: string | null = null; + public tableName: string | null = null; + public displayTableName: string | null = null; + public submitting: boolean = false; + public isSettingsExist: boolean = false; + public loading: boolean = true; + public fields: string[]; + public fields_to_exclude: string[]; + public orderChanged: boolean = false; + public iconChanged: boolean = false; + public listFieldsOrder: string[]; + public tableSettingsInitial: TableSettings = { + connection_id: '', + table_name: '', + icon: '', + display_name: '', + autocomplete_columns: [], + identity_column: '', + ordering: TableOrdering.Ascending, + ordering_field: '', + list_fields: [], + columns_view: [], + search_fields: [], + excluded_fields: [], + readonly_fields: [], + sortable_by: [], + sensitive_fields: [], + allow_csv_export: true, + allow_csv_import: true, + can_delete: true, + }; + public tableSettings: TableSettings = null; + public defaultIcons = [ + 'favorite', + 'star', + 'done', + 'arrow_forward', + 'key', + 'lock', + 'visibility', + 'language', + 'notifications', + 'schedule', + ]; + + constructor( + private _tables: TablesService, + private _connections: ConnectionsService, + private _location: Location, + private _company: CompanyService, + public router: Router, + private title: Title, + private angulartics2: Angulartics2, + ) {} + + ngOnInit(): void { + this.connectionID = this._connections.currentConnectionID; + this.tableName = this._tables.currentTableName; + this.displayTableName = normalizeTableName(this.tableName); + this._tables.cast.subscribe(); + this._tables.fetchTableStructure(this.connectionID, this.tableName).subscribe((res) => { + const primaryKeys = res.primaryColumns.map((primaryColumn) => primaryColumn.column_name); + this.fields = res.structure.map((field: TableField) => field.column_name); + this.fields_to_exclude = this.fields.filter((field) => !primaryKeys.includes(field)); + this.getTableSettings(); + }); + } + + get currentConnection() { + return this._connections.currentConnection; + } + + getCrumbs(name: string) { + return [ + { + label: name, + link: `/dashboard/${this.connectionID}`, + }, + { + label: this.tableSettings.display_name || this.displayTableName, + link: `/dashboard/${this.connectionID}/${this.tableName}`, + }, + { + label: 'Settings', + link: null, + }, + ]; + } + + goBack() { + this._location.back(); + } + + getTableSettings() { + this._tables.fetchTableSettings(this.connectionID, this.tableName).subscribe((res) => { + this.loading = false; + if (Object.keys(res).length !== 0) { + this.isSettingsExist = true; + this.tableSettings = res; + this.listFieldsOrder = [...res.list_fields]; + } else { + this.tableSettings = this.tableSettingsInitial; + } + if (Object.keys(res).length === 0 || (res && res.list_fields && !res.list_fields.length)) { + this.listFieldsOrder = [...this.fields]; + } + this.title.setTitle( + `${res.display_name || this.displayTableName} - Table settings | ${this._company.companyTabTitle || 'Rocketadmin'}`, + ); + }); + } + + updateIcon(icon: string) { + this.tableSettings.icon = icon; + this.iconChanged = true; + } + + drop(event: CdkDragDrop) { + moveItemInArray(this.listFieldsOrder, event.previousIndex, event.currentIndex); + this.tableSettings.list_fields = [...this.listFieldsOrder]; + this.orderChanged = true; + } + + resetColumnsOrder() { + this.tableSettings.list_fields = []; + this.listFieldsOrder = [...this.fields]; + this.orderChanged = true; + } + + updateSettings() { + this.submitting = true; + this.tableSettings.connection_id = this.connectionID; + this.tableSettings.table_name = this.tableName; + + const updatedSettings = {}; + + for (const [key, value] of Object.entries(this.tableSettings)) { + if (key !== 'connection_id' && key !== 'table_name' && key !== 'ordering') { + if (Array.isArray(value)) { + if (key === 'list_fields') { + updatedSettings[key] = this.orderChanged; + } else { + updatedSettings[key] = value.length > 0; + } + } else { + updatedSettings[key] = Boolean(value); + } + } + } + + this._tables + .updateTableSettings(this.isSettingsExist, this.connectionID, this.tableName, this.tableSettings) + .subscribe( + () => { + this.submitting = false; + this.angulartics2.eventTrack.next({ + action: 'Table settings: updated successfully', + properties: updatedSettings, + }); + posthog.capture('Table settings: updated successfully'); + this.router.navigate([`/dashboard/${this.connectionID}/${this.tableName}`]); + }, + () => { + this.submitting = false; + }, + () => { + this.submitting = false; + }, + ); + } + + resetSettings(form: NgForm) { + this.submitting = true; + this._tables.deleteTableSettings(this.connectionID, this.tableName).subscribe( + () => { + form.reset(); + this.submitting = false; + this.tableSettings = { ...this.tableSettingsInitial }; + this.angulartics2.eventTrack.next({ + action: 'Table settings: reset successfully', + }); + posthog.capture('Table settings: reset successfully'); + }, + () => { + this.submitting = false; + }, + () => { + this.submitting = false; + }, + ); + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index dd3f3ed53..f0210b7d2 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -52,6 +52,7 @@

{{ displayName }}

{{ displayName }}

matTooltip="AI assistant" angulartics2On="click" angularticsAction="Dashboard: AI is clicked" - (click)="handleViewAIpanel()"> + (click)="handleViewAIpanel(); posthog.capture('Dashboard: AI is clicked')"> AI insights @@ -79,7 +80,7 @@

{{ displayName }}

data-testid="table-add-record-link" angulartics2On="click" angularticsAction="Dashboard: add row is clicked" - (click)="stashUrlParams()"> + (click)="stashUrlParams(); posthog.capture('Dashboard: add row is clicked')"> add Add row @@ -88,14 +89,15 @@

{{ displayName }}

[ngClass]="{'action_active': getFiltersCount(activeFilters)}" angulartics2On="click" angularticsAction="Dashboard: filters is clicked" - (click)="handleOpenFilters()"> + (click)="handleOpenFilters(); posthog.capture('Dashboard: filters is clicked')"> filter_list Filter
@@ -150,7 +153,8 @@

{{ displayName }}

*ngIf="accessLevel === 'edit'" [matMenuTriggerFor]="settingsMenu" angulartics2On="click" - angularticsAction="Dashboard: settings dropdown is clicked"> + angularticsAction="Dashboard: settings dropdown is clicked" + (click)="posthog.capture('Dashboard: settings dropdown is clicked')"> more_vert @@ -158,14 +162,16 @@

{{ displayName }}

routerLink="/dashboard/{{connectionID}}/{{name}}/widgets" [ngClass]="{'action_active': tableData.widgetsCount}" angulartics2On="click" - angularticsAction="Dashboard: widgets is clicked"> + angularticsAction="Dashboard: widgets is clicked" + (click)="posthog.capture('Dashboard: widgets is clicked')"> UI Widgets + angularticsAction="Dashboard: actions is clicked" + (click)="posthog.capture('Dashboard: actions is clicked')"> Automations
@@ -175,7 +181,8 @@

{{ displayName }}

+ angularticsAction="Dashboard: settings is clicked" + (click)="posthog.capture('Dashboard: settings is clicked')"> Settings
@@ -349,7 +356,7 @@

{{ displayName }}

angulartics2On="click" angularticsAction="Dashboard: edit row is clicked" matTooltip="Edit row" - (click)="stashUrlParams()"> + (click)="stashUrlParams(); posthog.capture('Dashboard: edit row is clicked')"> create {{ displayName }}

angulartics2On="click" angularticsAction="Dashboard: duplicate row is clicked" matTooltip="Duplicate row" - (click)="stashUrlParams()"> + (click)="stashUrlParams(); posthog.capture('Dashboard: duplicate row is clicked')"> difference

angulartics2On="click" angularticsAction="Dashboard: delete row is clicked" matTooltip="Delete row" - (click)="handleDeleteRow($event, element)"> + (click)="handleDeleteRow($event, element); posthog.capture('Dashboard: delete row is clicked')"> delete @@ -403,6 +410,7 @@

{{ displayName }}

routerLink="/dashboard/{{connectionID}}/{{name}}/entry" angulartics2On="click" angularticsAction="Dashboard: add row is clicked" + (click)="posthog.capture('Dashboard: add row is clicked')" data-testid="table-add-record-in-empty-table-link"> add Add row diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 7d64e516a..d1c292331 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -35,6 +35,7 @@ import { Angulartics2OnModule } from 'angulartics2'; import JSON5 from 'json5'; import { DynamicModule } from 'ng-dynamic-component'; import { SignalComponentIoModule } from 'ng-dynamic-component/signal-component-io'; +import posthog from 'posthog-js'; import { merge } from 'rxjs'; import { tap } from 'rxjs/operators'; import { formatFieldValue } from 'src/app/lib/format-field-value'; @@ -110,6 +111,7 @@ export interface Folder { ], }) export class DbTableViewComponent implements OnInit, OnChanges { + protected posthog = posthog; @Input() name: string; @Input() displayName: string; @Input() permissions: TablePermissions; diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html index 98311f3d3..f8a0dd141 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.html @@ -17,7 +17,7 @@ type="button" angulartics2On="click" angularticsAction="Widgets: add widget is clicked" - (click)="addNewWidget()" + (click)="addNewWidget(); posthog.capture('Widgets: add widget is clicked')" [disabled]="fields.length === 0 || widgets.length === fieldsCount"> add Add UI widget @@ -84,7 +84,7 @@ ; - - public tableRowFields: Object; - public tableRowStructure: Object; - public tableRowFieldsShown: Object = {}; - public tableRowFieldsComparator: Object = {}; - // public tableForeignKeys: {[key: string]: TableForeignKey}; - public tableFiltersCount: number = 0; - public tableTypes: Object; - public tableWidgets: object; - public tableWidgetsList: string[] = []; - public UIwidgets = UIwidgets; - public dynamicColumn: string | null = null; - public showAddConditionField = false; - public showNameError = false; - public showConditionsError = false; - - @ViewChild('tableFiltersForm') tableFiltersForm: any; - - constructor( - @Inject(MAT_DIALOG_DATA) public data: any, - private _tables: TablesService, - private _connections: ConnectionsService, - private dialogRef: MatDialogRef, - private snackBar: MatSnackBar, - private angulartics2: Angulartics2, - private elementRef: ElementRef, - ) {} - - ngOnInit(): void { - this._tables.cast.subscribe(); - - if (this.data.filtersSet) { - this.tableRowFieldsShown = Object.entries(this.data.filtersSet.filters).reduce((acc, [field, conditions]) => { - const [_comparator, value] = Object.entries(conditions)[0]; - acc[field] = value; - return acc; - }, {}); - - this.tableRowFieldsComparator = Object.entries(this.data.filtersSet.filters).reduce((acc, [field, conditions]) => { - const [comparator] = Object.keys(conditions); - acc[field] = comparator; - return acc; - }, {}); - - // Initialize dynamic column if it exists in the filters set - if (this.data.filtersSet.dynamic_column?.column_name) { - this.tableRowFieldsShown[this.data.filtersSet.dynamic_column.column_name] = null; - this.tableRowFieldsComparator[this.data.filtersSet.dynamic_column.column_name] = this.data.filtersSet.dynamic_column.comparator || ''; - this.dynamicColumn = this.data.filtersSet.dynamic_column.column_name; - } - } - - // this.tableForeignKeys = {...this.data.structure.foreignKeys}; - this.tableRowFields = Object.assign({}, ...this.data.structure.map((field: TableField) => ({[field.column_name]: undefined}))); - const foreignKeysList = Object.keys(this.data.tableForeignKeys); - this.tableTypes = getTableTypes(this.data.structure, foreignKeysList); - this.fields = this.data.structure - .filter((field: TableField) => this.getInputType(field.column_name) !== 'file') - .map((field: TableField) => field.column_name); - - this.tableRowStructure = Object.assign({}, ...this.data.structure.map((field: TableField) => { - return {[field.column_name]: field}; - })); - - // Setup widgets if available - if (this.data.tableWidgets?.length) { - this.setWidgets(this.data.tableWidgets); - } - - this.foundFields = this.fieldSearchControl.valueChanges.pipe( - startWith(''), - map(value => this._filter(value || '')), - ); - } - - ngAfterViewInit(): void { - // If editing an existing filter (has id), remove focus from the filter name input - if (this.data.filtersSet && this.data.filtersSet.id) { - setTimeout(() => { - const nameInput = this.elementRef.nativeElement.querySelector('input[name="filters_set_name"]') as HTMLInputElement; - if (nameInput && document.activeElement === nameInput) { - nameInput.blur(); - } - }, 100); - } - } - - get hasSelectedFilters(): boolean { - return Object.keys(this.tableRowFieldsShown).length > 0; - } - - handleAddConditionButtonClick(): void { - this.showAddConditionField = true; - setTimeout(() => { - const input = this.elementRef.nativeElement.querySelector('input[name="filter_columns"]') as HTMLInputElement; - input?.focus(); - }, 0); - } - - cancelAddConditionInput(): void { - this.showAddConditionField = false; - this.fieldSearchControl.setValue(''); - } - - private _filter(value: string): string[] { - return this.fields.filter((field: string) => field.toLowerCase().includes(value.toLowerCase())); - } - - get inputs() { - return filterTypes[this._connections.currentConnection.type]; - } - - setWidgets(widgets: any[]) { - this.tableWidgetsList = widgets.map((widget: any) => widget.field_name); - this.tableWidgets = Object.assign({}, ...widgets - .map((widget: any) => { - let params; - if (widget.widget_params !== '// No settings required') { - try { - params = JSON.parse(widget.widget_params); - } catch (_e) { - params = ''; - } - } else { - params = ''; - } - return { - [widget.field_name]: {...widget, widget_params: params} - }; - }) - ); - } - - trackByFn(_index: number, item: any) { - return item.key; - } - - isWidget(columnName: string) { - return this.tableWidgetsList.includes(columnName); - } - - updateField = (updatedValue: any, field: string) => { - this.tableRowFieldsShown[field] = updatedValue; - this.updateFiltersCount(); - // Reset conditions error when a filter is added - if (this.showConditionsError && Object.keys(this.tableRowFieldsShown).length > 0) { - this.showConditionsError = false; - } - } - - addFilter(e) { - const key = e.option.value; - this.tableRowFieldsShown = {...this.tableRowFieldsShown, [key]: this.tableRowFields[key]}; - this.tableRowFieldsComparator = {...this.tableRowFieldsComparator, [key]: this.tableRowFieldsComparator[key] || 'eq'}; - this.fieldSearchControl.setValue(''); - this.updateFiltersCount(); - // Reset conditions error when a filter is added - this.showConditionsError = false; - if (this.hasSelectedFilters) { - this.showAddConditionField = false; - } - } - - handleInputBlur(): void { - // Hide the field if it's empty when it loses focus - if (!this.fieldSearchControl.value || this.fieldSearchControl.value.trim() === '') { - setTimeout(() => { - if (!this.fieldSearchControl.value || this.fieldSearchControl.value.trim() === '') { - this.cancelAddConditionInput(); - } - }, 200); - } - } - - updateComparator(event, fieldName: string) { - console.log('Updating comparator for field:', fieldName, 'obj', this.tableRowFieldsComparator); - if (event === 'empty') this.tableRowFieldsShown[fieldName] = ''; - } - - removeFilters() { - this._tables.deleteSavedFilter(this.data.connectionID, this.data.tableName, this.data.filtersSet.id).subscribe({ - next: () => { - this.dialogRef.close(true); - }, - error: (error) => { - console.error('Error removing filters:', error); - this.snackBar.open('Error removing filters', 'Close', { duration: 3000 }); - } - }); - } - - getInputType(field: string) { - let widgetType; - if (this.isWidget(field)) { - widgetType = this.UIwidgets[this.tableWidgets[field].widget_type]?.type; - } else { - widgetType = this.inputs[this.tableTypes[field]]?.type; - } - return widgetType; - } - - getComparatorType(typeOfComponent) { - if (typeOfComponent === 'text') { - return 'text'; - } else if (typeOfComponent === 'number' || typeOfComponent === 'datetime') { - return 'number'; - } else { - return 'nonComparable'; - } - } - - getOperatorIcon(operator: string): string { - const iconMap: { [key: string]: string } = { - 'startswith': 'play_arrow', - 'endswith': 'play_arrow', - 'eq': 'drag_handle', - 'contains': 'search', - 'icontains': 'block', - 'empty': 'space_bar', - 'gt': 'keyboard_arrow_right', - 'lt': 'keyboard_arrow_left', - 'gte': 'keyboard_double_arrow_right', - 'lte': 'keyboard_double_arrow_left' - }; - return iconMap[operator] || 'drag_handle'; - } - - removeFilter(field) { - delete this.tableRowFieldsShown[field]; - delete this.tableRowFieldsComparator[field]; - if (this.dynamicColumn === field) { - this.dynamicColumn = null; - } - this.updateFiltersCount(); - // Reset conditions error when filters are removed (will be re-validated on save) - this.showConditionsError = false; - if (!this.hasSelectedFilters) { - this.showAddConditionField = false; - } - } - - updateFiltersCount() { - this.tableFiltersCount = Object.keys(this.tableRowFieldsShown).length; - } - - toggleDynamicColumn(field: string) { - if (this.dynamicColumn === field) { - this.dynamicColumn = null; - } else { - this.dynamicColumn = field; - } - } - - handleSaveFilters() { - // Reset error flags - this.showNameError = false; - this.showConditionsError = false; - - // Validate filter name - if (!this.data.filtersSet.name || this.data.filtersSet.name.trim() === '') { - this.showNameError = true; - setTimeout(() => { - const nameInput = this.elementRef.nativeElement.querySelector('input[name="filters_set_name"]') as HTMLInputElement; - if (nameInput) { - nameInput.focus(); - nameInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - }, 0); - return; - } - - // Validate conditions - check if there are any filters - // A valid filter must have a comparator defined - // Either regular filters OR dynamic column with comparator should exist - const hasRegularFilters = Object.keys(this.tableRowFieldsShown).some(key => { - // Skip dynamic column for regular filter check - if (key === this.dynamicColumn) { - return false; - } - // Check if comparator is defined (even if value is empty/null, comparator must exist) - return this.tableRowFieldsComparator[key] !== undefined && this.tableRowFieldsComparator[key] !== null; - }); - - // Check if dynamic column has a comparator (it counts as a valid filter condition) - const hasDynamicColumnFilter = this.dynamicColumn && - this.tableRowFieldsComparator[this.dynamicColumn] !== undefined && - this.tableRowFieldsComparator[this.dynamicColumn] !== null; - - if (!hasRegularFilters && !hasDynamicColumnFilter) { - this.showConditionsError = true; - setTimeout(() => { - const conditionInput = this.elementRef.nativeElement.querySelector('input[name="filter_columns"]') as HTMLInputElement; - if (conditionInput) { - conditionInput.focus(); - conditionInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } else { - // If input is not visible, show the add condition button area - const addButton = this.elementRef.nativeElement.querySelector('.add-condition-footer button') as HTMLElement; - if (addButton) { - addButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - } - }, 0); - return; - } - - let payload; - if (Object.keys(this.tableRowFieldsShown).length) { - let filters = {}; - - for (const key in this.tableRowFieldsShown) { - // Skip fields that are marked as dynamic column - if (key === this.dynamicColumn) { - continue; - } - - if (this.tableRowFieldsComparator[key] !== undefined) { - // If value is empty or undefined, use null - const value = this.tableRowFieldsShown[key] === '' || this.tableRowFieldsShown[key] === undefined ? - null : this.tableRowFieldsShown[key]; - - filters[key] = { - [this.tableRowFieldsComparator[key]]: value - }; - } - } - - // const filters = JsonURL.stringify( this.filters ); - payload = { - name: this.data.filtersSet.name, - filters - }; - - // Only add dynamic_column if one is selected - if (this.dynamicColumn) { - // Create object with column_name and comparator properties - payload.dynamic_column = { - column_name: this.dynamicColumn, - comparator: this.tableRowFieldsComparator[this.dynamicColumn] || '' - }; - } - - if (this.data.filtersSet.id) { - this._tables.updateSavedFilter(this.data.connectionID, this.data.tableName, this.data.filtersSet.id, payload) - .subscribe(() => { - this.dialogRef.close(true); - }, (error) => { - console.error('Error updating filter:', error); - this.snackBar.open('Error updating filter', 'Close', { duration: 3000 }); - }); - } else { - this._tables.createSavedFilter(this.data.connectionID, this.data.tableName, payload) - .subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Saved filters: saved filter is created successfully', - }); - this.dialogRef.close(true); - }, (error) => { - this.angulartics2.eventTrack.next({ - action: 'Saved filters: error creating saved filter', - }); - console.error('Error saving filter:', error); - this.snackBar.open('Error saving filter', 'Close', { duration: 3000 }); - }); - } - } - - // saveFilter() { - - - // this._tables.createSavedFilter(this.data.connectionID, this.data.tableName, payload) - // .subscribe(() => { - // this.dialogRef.close(true); - // }, (error) => { - // console.error('Error saving filter:', error); - // this.snackBar.open('Error saving filter', 'Close', { duration: 3000 }); - // }); - // } - } -} \ No newline at end of file + // @Input() connectionID: string; + // @Input() tableName: string; + // @Input() displayTableName: string; + // @Input() filtersSet: any; + + public tableFilters = []; + public fieldSearchControl = new FormControl(''); + public fields: string[]; + public foundFields: Observable; + + public tableRowFields: Object; + public tableRowStructure: Object; + public tableRowFieldsShown: Object = {}; + public tableRowFieldsComparator: Object = {}; + // public tableForeignKeys: {[key: string]: TableForeignKey}; + public tableFiltersCount: number = 0; + public tableTypes: Object; + public tableWidgets: object; + public tableWidgetsList: string[] = []; + public UIwidgets = UIwidgets; + public dynamicColumn: string | null = null; + public showAddConditionField = false; + public showNameError = false; + public showConditionsError = false; + + @ViewChild('tableFiltersForm') tableFiltersForm: any; + + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + private _tables: TablesService, + private _connections: ConnectionsService, + private dialogRef: MatDialogRef, + private snackBar: MatSnackBar, + private angulartics2: Angulartics2, + private elementRef: ElementRef, + ) {} + + ngOnInit(): void { + this._tables.cast.subscribe(); + + if (this.data.filtersSet) { + this.tableRowFieldsShown = Object.entries(this.data.filtersSet.filters).reduce((acc, [field, conditions]) => { + const [_comparator, value] = Object.entries(conditions)[0]; + acc[field] = value; + return acc; + }, {}); + + this.tableRowFieldsComparator = Object.entries(this.data.filtersSet.filters).reduce( + (acc, [field, conditions]) => { + const [comparator] = Object.keys(conditions); + acc[field] = comparator; + return acc; + }, + {}, + ); + + // Initialize dynamic column if it exists in the filters set + if (this.data.filtersSet.dynamic_column?.column_name) { + this.tableRowFieldsShown[this.data.filtersSet.dynamic_column.column_name] = null; + this.tableRowFieldsComparator[this.data.filtersSet.dynamic_column.column_name] = + this.data.filtersSet.dynamic_column.comparator || ''; + this.dynamicColumn = this.data.filtersSet.dynamic_column.column_name; + } + } + + // this.tableForeignKeys = {...this.data.structure.foreignKeys}; + this.tableRowFields = Object.assign( + {}, + ...this.data.structure.map((field: TableField) => ({ [field.column_name]: undefined })), + ); + const foreignKeysList = Object.keys(this.data.tableForeignKeys); + this.tableTypes = getTableTypes(this.data.structure, foreignKeysList); + this.fields = this.data.structure + .filter((field: TableField) => this.getInputType(field.column_name) !== 'file') + .map((field: TableField) => field.column_name); + + this.tableRowStructure = Object.assign( + {}, + ...this.data.structure.map((field: TableField) => { + return { [field.column_name]: field }; + }), + ); + + // Setup widgets if available + if (this.data.tableWidgets?.length) { + this.setWidgets(this.data.tableWidgets); + } + + this.foundFields = this.fieldSearchControl.valueChanges.pipe( + startWith(''), + map((value) => this._filter(value || '')), + ); + } + + ngAfterViewInit(): void { + // If editing an existing filter (has id), remove focus from the filter name input + if (this.data.filtersSet && this.data.filtersSet.id) { + setTimeout(() => { + const nameInput = this.elementRef.nativeElement.querySelector( + 'input[name="filters_set_name"]', + ) as HTMLInputElement; + if (nameInput && document.activeElement === nameInput) { + nameInput.blur(); + } + }, 100); + } + } + + get hasSelectedFilters(): boolean { + return Object.keys(this.tableRowFieldsShown).length > 0; + } + + handleAddConditionButtonClick(): void { + this.showAddConditionField = true; + setTimeout(() => { + const input = this.elementRef.nativeElement.querySelector('input[name="filter_columns"]') as HTMLInputElement; + input?.focus(); + }, 0); + } + + cancelAddConditionInput(): void { + this.showAddConditionField = false; + this.fieldSearchControl.setValue(''); + } + + private _filter(value: string): string[] { + return this.fields.filter((field: string) => field.toLowerCase().includes(value.toLowerCase())); + } + + get inputs() { + return filterTypes[this._connections.currentConnection.type]; + } + + setWidgets(widgets: any[]) { + this.tableWidgetsList = widgets.map((widget: any) => widget.field_name); + this.tableWidgets = Object.assign( + {}, + ...widgets.map((widget: any) => { + let params; + if (widget.widget_params !== '// No settings required') { + try { + params = JSON.parse(widget.widget_params); + } catch (_e) { + params = ''; + } + } else { + params = ''; + } + return { + [widget.field_name]: { ...widget, widget_params: params }, + }; + }), + ); + } + + trackByFn(_index: number, item: any) { + return item.key; + } + + isWidget(columnName: string) { + return this.tableWidgetsList.includes(columnName); + } + + updateField = (updatedValue: any, field: string) => { + this.tableRowFieldsShown[field] = updatedValue; + this.updateFiltersCount(); + // Reset conditions error when a filter is added + if (this.showConditionsError && Object.keys(this.tableRowFieldsShown).length > 0) { + this.showConditionsError = false; + } + }; + + addFilter(e) { + const key = e.option.value; + this.tableRowFieldsShown = { ...this.tableRowFieldsShown, [key]: this.tableRowFields[key] }; + this.tableRowFieldsComparator = { + ...this.tableRowFieldsComparator, + [key]: this.tableRowFieldsComparator[key] || 'eq', + }; + this.fieldSearchControl.setValue(''); + this.updateFiltersCount(); + // Reset conditions error when a filter is added + this.showConditionsError = false; + if (this.hasSelectedFilters) { + this.showAddConditionField = false; + } + } + + handleInputBlur(): void { + // Hide the field if it's empty when it loses focus + if (!this.fieldSearchControl.value || this.fieldSearchControl.value.trim() === '') { + setTimeout(() => { + if (!this.fieldSearchControl.value || this.fieldSearchControl.value.trim() === '') { + this.cancelAddConditionInput(); + } + }, 200); + } + } + + updateComparator(event, fieldName: string) { + console.log('Updating comparator for field:', fieldName, 'obj', this.tableRowFieldsComparator); + if (event === 'empty') this.tableRowFieldsShown[fieldName] = ''; + } + + removeFilters() { + this._tables.deleteSavedFilter(this.data.connectionID, this.data.tableName, this.data.filtersSet.id).subscribe({ + next: () => { + this.dialogRef.close(true); + }, + error: (error) => { + console.error('Error removing filters:', error); + this.snackBar.open('Error removing filters', 'Close', { duration: 3000 }); + }, + }); + } + + getInputType(field: string) { + let widgetType; + if (this.isWidget(field)) { + widgetType = this.UIwidgets[this.tableWidgets[field].widget_type]?.type; + } else { + widgetType = this.inputs[this.tableTypes[field]]?.type; + } + return widgetType; + } + + getComparatorType(typeOfComponent) { + if (typeOfComponent === 'text') { + return 'text'; + } else if (typeOfComponent === 'number' || typeOfComponent === 'datetime') { + return 'number'; + } else { + return 'nonComparable'; + } + } + + getOperatorIcon(operator: string): string { + const iconMap: { [key: string]: string } = { + startswith: 'play_arrow', + endswith: 'play_arrow', + eq: 'drag_handle', + contains: 'search', + icontains: 'block', + empty: 'space_bar', + gt: 'keyboard_arrow_right', + lt: 'keyboard_arrow_left', + gte: 'keyboard_double_arrow_right', + lte: 'keyboard_double_arrow_left', + }; + return iconMap[operator] || 'drag_handle'; + } + + removeFilter(field) { + delete this.tableRowFieldsShown[field]; + delete this.tableRowFieldsComparator[field]; + if (this.dynamicColumn === field) { + this.dynamicColumn = null; + } + this.updateFiltersCount(); + // Reset conditions error when filters are removed (will be re-validated on save) + this.showConditionsError = false; + if (!this.hasSelectedFilters) { + this.showAddConditionField = false; + } + } + + updateFiltersCount() { + this.tableFiltersCount = Object.keys(this.tableRowFieldsShown).length; + } + + toggleDynamicColumn(field: string) { + if (this.dynamicColumn === field) { + this.dynamicColumn = null; + } else { + this.dynamicColumn = field; + } + } + + handleSaveFilters() { + // Reset error flags + this.showNameError = false; + this.showConditionsError = false; + + // Validate filter name + if (!this.data.filtersSet.name || this.data.filtersSet.name.trim() === '') { + this.showNameError = true; + setTimeout(() => { + const nameInput = this.elementRef.nativeElement.querySelector( + 'input[name="filters_set_name"]', + ) as HTMLInputElement; + if (nameInput) { + nameInput.focus(); + nameInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 0); + return; + } + + // Validate conditions - check if there are any filters + // A valid filter must have a comparator defined + // Either regular filters OR dynamic column with comparator should exist + const hasRegularFilters = Object.keys(this.tableRowFieldsShown).some((key) => { + // Skip dynamic column for regular filter check + if (key === this.dynamicColumn) { + return false; + } + // Check if comparator is defined (even if value is empty/null, comparator must exist) + return this.tableRowFieldsComparator[key] !== undefined && this.tableRowFieldsComparator[key] !== null; + }); + + // Check if dynamic column has a comparator (it counts as a valid filter condition) + const hasDynamicColumnFilter = + this.dynamicColumn && + this.tableRowFieldsComparator[this.dynamicColumn] !== undefined && + this.tableRowFieldsComparator[this.dynamicColumn] !== null; + + if (!hasRegularFilters && !hasDynamicColumnFilter) { + this.showConditionsError = true; + setTimeout(() => { + const conditionInput = this.elementRef.nativeElement.querySelector( + 'input[name="filter_columns"]', + ) as HTMLInputElement; + if (conditionInput) { + conditionInput.focus(); + conditionInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } else { + // If input is not visible, show the add condition button area + const addButton = this.elementRef.nativeElement.querySelector('.add-condition-footer button') as HTMLElement; + if (addButton) { + addButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + }, 0); + return; + } + + let payload; + if (Object.keys(this.tableRowFieldsShown).length) { + let filters = {}; + + for (const key in this.tableRowFieldsShown) { + // Skip fields that are marked as dynamic column + if (key === this.dynamicColumn) { + continue; + } + + if (this.tableRowFieldsComparator[key] !== undefined) { + // If value is empty or undefined, use null + const value = + this.tableRowFieldsShown[key] === '' || this.tableRowFieldsShown[key] === undefined + ? null + : this.tableRowFieldsShown[key]; + + filters[key] = { + [this.tableRowFieldsComparator[key]]: value, + }; + } + } + + // const filters = JsonURL.stringify( this.filters ); + payload = { + name: this.data.filtersSet.name, + filters, + }; + + // Only add dynamic_column if one is selected + if (this.dynamicColumn) { + // Create object with column_name and comparator properties + payload.dynamic_column = { + column_name: this.dynamicColumn, + comparator: this.tableRowFieldsComparator[this.dynamicColumn] || '', + }; + } + + if (this.data.filtersSet.id) { + this._tables + .updateSavedFilter(this.data.connectionID, this.data.tableName, this.data.filtersSet.id, payload) + .subscribe( + () => { + this.dialogRef.close(true); + }, + (error) => { + console.error('Error updating filter:', error); + this.snackBar.open('Error updating filter', 'Close', { duration: 3000 }); + }, + ); + } else { + this._tables.createSavedFilter(this.data.connectionID, this.data.tableName, payload).subscribe( + () => { + this.angulartics2.eventTrack.next({ + action: 'Saved filters: saved filter is created successfully', + }); + posthog.capture('Saved filters: saved filter is created successfully'); + this.dialogRef.close(true); + }, + (error) => { + this.angulartics2.eventTrack.next({ + action: 'Saved filters: error creating saved filter', + }); + posthog.capture('Saved filters: error creating saved filter'); + console.error('Error saving filter:', error); + this.snackBar.open('Error saving filter', 'Close', { duration: 3000 }); + }, + ); + } + } + + // saveFilter() { + + // this._tables.createSavedFilter(this.data.connectionID, this.data.tableName, payload) + // .subscribe(() => { + // this.dialogRef.close(true); + // }, (error) => { + // console.error('Error saving filter:', error); + // this.snackBar.open('Error saving filter', 'Close', { duration: 3000 }); + // }); + // } + } +} diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html index 684911e0d..92fd2885b 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.html @@ -6,7 +6,7 @@ mat-button type="button" angulartics2On="click" angularticsAction="Saved filters: first time setup is clicked" - (click)="handleOpenSavedFiltersDialog()"> + (click)="handleOpenSavedFiltersDialog(); posthog.capture('Saved filters: first time setup is clicked')"> tune Create fast filter @@ -15,7 +15,7 @@ mat-icon-button type="button" class="create-filter-button" matTooltip="Create new fast filter" - (click)="handleOpenSavedFiltersDialog()" + (click)="handleOpenSavedFiltersDialog(); posthog.capture('Saved filters: create new filter is clicked')" angulartics2On="click" angularticsAction="Saved filters: create new filter is clicked"> add diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts index 60a5274c8..6b38723f9 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts @@ -1,14 +1,6 @@ -import { ActivatedRoute, Router } from '@angular/router'; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { TableField, TableForeignKey } from 'src/app/models/table'; - -import { AccessLevel } from 'src/app/models/user'; -import { Angulartics2OnModule } from 'angulartics2'; import { CommonModule } from '@angular/common'; -import { ConnectionsService } from 'src/app/services/connections.service'; -import { DynamicModule } from 'ng-dynamic-component'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import JsonURL from '@jsonurl/jsonurl'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; import { MatDialog } from '@angular/material/dialog'; @@ -19,587 +11,608 @@ import { MatMenuModule } from '@angular/material/menu'; import { MatSelectModule } from '@angular/material/select'; import { MatTabsModule } from '@angular/material/tabs'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { ActivatedRoute, Router } from '@angular/router'; +import JsonURL from '@jsonurl/jsonurl'; +import { Angulartics2OnModule } from 'angulartics2'; +import { DynamicModule } from 'ng-dynamic-component'; +import posthog from 'posthog-js'; import { PlaceholderSavedFiltersComponent } from 'src/app/components/skeletons/placeholder-saved-filters/placeholder-saved-filters.component'; -import { SavedFiltersDialogComponent } from './saved-filters-dialog/saved-filters-dialog.component'; -import { TablesService } from 'src/app/services/tables.service'; -import { UIwidgets } from 'src/app/consts/record-edit-types'; import { filterTypes } from 'src/app/consts/filter-types'; +import { UIwidgets } from 'src/app/consts/record-edit-types'; import { normalizeTableName } from 'src/app/lib/normalize'; +import { TableField, TableForeignKey } from 'src/app/models/table'; +import { AccessLevel } from 'src/app/models/user'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { TablesService } from 'src/app/services/tables.service'; +import { SavedFiltersDialogComponent } from './saved-filters-dialog/saved-filters-dialog.component'; @Component({ - selector: 'app-saved-filters-panel', - imports: [ - CommonModule, - FormsModule, - DynamicModule, - MatButtonModule, - MatIconModule, - MatChipsModule, - MatFormFieldModule, - MatInputModule, - MatSelectModule, - MatTabsModule, - MatTooltipModule, - MatMenuModule, - PlaceholderSavedFiltersComponent, - Angulartics2OnModule - ], - templateUrl: './saved-filters-panel.component.html', - styleUrl: './saved-filters-panel.component.css' + selector: 'app-saved-filters-panel', + imports: [ + CommonModule, + FormsModule, + DynamicModule, + MatButtonModule, + MatIconModule, + MatChipsModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatTabsModule, + MatTooltipModule, + MatMenuModule, + PlaceholderSavedFiltersComponent, + Angulartics2OnModule, + ], + templateUrl: './saved-filters-panel.component.html', + styleUrl: './saved-filters-panel.component.css', }) export class SavedFiltersPanelComponent implements OnInit, OnDestroy { - @Input() connectionID: string; - @Input() selectedTableName: string; - @Input() selectedTableDisplayName: string; - @Input() tableTypes: any; - @Input() structure: any; - @Input() tableForeignKeys: TableForeignKey[] = []; - @Input() tableWidgets: any = {}; - // @Input() savedFilterData: any; - @Output() filterSelected = new EventEmitter(); - @Input() resetSelection: boolean = false; - - @Input() accessLevel: AccessLevel; - - private dynamicColumnValueDebounceTimer: any = null; - - public savedFilterData = null; - public savedFilterMap: { [key: string]: any } = {}; - - public selectedFilterSetId: string | null = null; - public selectedFilter: any = null; - public shouldAutofocus: boolean = false; - public currentFilterForMenu: any = null; - - public tableStructure: any = null; - public tableRowFieldsShown: { [key: string]: any } = {}; - public tableRowStructure: { [key: string]: any } = {}; - public tableWidgetsList: string[] = []; - public UIwidgets = UIwidgets; - - public displayedComparators = { - eq: "=", - gt: ">", - lt: "<", - gte: ">=", - lte: "<=", - startswith: "starts with", - endswith: "ends with", - contains: "contains", - icontains: "not contains", - empty: "is empty" - } - - constructor( - private dialog: MatDialog, - private _tables: TablesService, - private _connections: ConnectionsService, - private router: Router, - private route: ActivatedRoute - ) {} - - ngOnInit() { - this.route.paramMap.subscribe(params => { - const tableNameFromUrl = params.get('table-name'); - if (tableNameFromUrl) { - this.savedFilterData = null; - this.selectedFilterSetId = null; - this.selectedFilter = null; - this.selectedTableName = tableNameFromUrl; - this.loadSavedFilters(); - } - }); - - this.route.queryParamMap.subscribe(params => { - const savedFilterId = params.get('saved_filter'); - if (savedFilterId) { - this.selectedFilterSetId = savedFilterId; - } else { - this.selectedFilterSetId = null; - } - }); - - this._tables.cast.subscribe((arg) => { - if (arg === 'filters set saved') { - this.loadSavedFilters(); - } - - if (arg === 'filters set updated') { - // Get the current saved filter ID from URL - const savedFilterIdFromUrl = this.route.snapshot.queryParams.saved_filter; - - if (savedFilterIdFromUrl) { - // If we have a filter selected in URL, get latest data and update URL - this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ - next: (data) => { - if (data) { - // Find the updated filter in the response - const updatedFilter = data.find(filter => filter.id === savedFilterIdFromUrl); - if (updatedFilter) { - const processedFilter = this.processFiltersData(updatedFilter); - - // Update local state - this.selectedFilterSetId = savedFilterIdFromUrl; - this.filterSelected.emit(processedFilter); - - // Update URL with the refreshed filter data - const additionalParams: any = { - filters: JsonURL.stringify(updatedFilter.filters), - saved_filter: savedFilterIdFromUrl - }; - - if (updatedFilter.dynamic_column) { - additionalParams.dynamic_column = JsonURL.stringify({ - column_name: updatedFilter.dynamic_column.column_name, - comparator: updatedFilter.dynamic_column.comparator - }); - } - - const queryParams = this.buildQueryParams(additionalParams); - this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); - } - - // Update the full filters map - this.savedFilterData = data.sort((a, b) => - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() - ); - this.savedFilterMap = Object.assign({}, ...data.map((filter) => { - const transformedFilter = this.processFiltersData(filter); - return { [filter.id]: transformedFilter }; - })); - } - }, - error: (error) => { - console.error('Error fetching updated filters:', error); - this.loadSavedFilters(); - } - }); - } else { - // Just refresh filters if no filter selected in URL - this.loadSavedFilters(); - } - } - - if (arg === 'delete saved filters') { - this.loadSavedFilters(); - this.selectedFilterSetId = null; - this.filterSelected.emit(null); - const queryParams = this.buildQueryParams(); - this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); - } - }); - - this.tableRowStructure = Object.assign({}, ...this.structure.map((field: TableField) => { - return {[field.column_name]: field}; - })); - } - - loadSavedFilters() { - if (!this.connectionID || !this.selectedTableName) { - return; - } - - this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ - next: (data) => { - if (data) { - this.savedFilterData = data.sort((a, b) => - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() - ); - this.savedFilterMap = Object.assign({}, ...data.map((filter) => { - const transformedFilter = this.processFiltersData(filter); - return { [filter.id]: transformedFilter }; - })); - - const params = this.route.snapshot.queryParams; - const dynamicColumn = params.dynamic_column ? JsonURL.parse(params.dynamic_column) : null; - - if (this.selectedFilterSetId && this.savedFilterData.length > 0) { - if (dynamicColumn && this.savedFilterMap[this.selectedFilterSetId]) { - const filters = params.filters ? JsonURL.parse(params.filters) : {}; - - this.savedFilterMap[this.selectedFilterSetId].dynamicColumn = { - column: dynamicColumn.column_name, - operator: dynamicColumn.comparator, - value: filters[dynamicColumn.column_name]?.[dynamicColumn.comparator] || null - }; - } - } - } - }, - error: (error) => { - console.error('Error fetching saved filters:', error); - } - }); - } - - ngOnChanges() { - if (this.resetSelection) { - this.selectedFilterSetId = null; - } - } - - ngOnDestroy() { - if (this.dynamicColumnValueDebounceTimer) { - clearTimeout(this.dynamicColumnValueDebounceTimer); - } - } - - handleOpenSavedFiltersDialog(filtersSet: any = null) { - const _dialogRef = this.dialog.open(SavedFiltersDialogComponent, { - width: '56em', - data: { - connectionID: this.connectionID, - tableName: this.selectedTableName, - displayTableName: this.selectedTableDisplayName, - structure: this.structure, - tableForeignKeys: this.tableForeignKeys, - tableWidgets: this.tableWidgets, - filtersSet: filtersSet ? filtersSet : { - name: '', - filters: {} - } - } - }); - - // No need to handle URL updates here - it's now handled in the tables.cast subscription - // when 'filters set updated' is received - } - - setCurrentFilter(filter: any) { - this.currentFilterForMenu = filter; - } - - handleEditFilter(filter: any) { - if (filter) { - this.handleOpenSavedFiltersDialog(filter); - } - } - - handleDeleteFilter(filter: any) { - if (filter) { - this._tables.deleteSavedFilter(this.connectionID, this.selectedTableName, filter.id).subscribe({ - next: () => { - // The deletion will trigger 'delete saved filters' event which will refresh the list - }, - error: (error) => { - console.error('Error deleting filter:', error); - } - }); - } - } - - getFilterEntries(filters: any): { column: string; operator: string; value: string }[] { - if (!filters) return []; - - const entries: { column: string; operator: string; value: string }[] = []; - - Object.keys(filters).forEach(column => { - const operations = filters[column]; - Object.keys(operations).forEach(operator => { - entries.push({ - column, - operator, - value: operations[operator] - }); - }); - }); - return entries; - } - - processFiltersData(filter: any) { - const transformedFilter = { - ...filter, - filterEntries: this.getFilterEntries(filter.filters), - staticFilters: [] as { column: string; operator: string; value: any }[], - dynamicColumn: null as { column: string; operator: string; value: any } | null - }; - - if (filter.dynamic_column?.column_name) { - transformedFilter.dynamicColumn = { - column: filter.dynamic_column.column_name, - operator: filter.dynamic_column.comparator, - value: null - }; - - const dynamicColFilters = filter.filters?.[filter.dynamic_column.column_name]; - if (dynamicColFilters) { - const operator = filter.dynamic_column.comparator; - transformedFilter.dynamicColumn.value = dynamicColFilters[operator]; - } - } - - transformedFilter.staticFilters = this.getFilterEntries(filter.filters) - .filter(entry => !filter.dynamic_column || entry.column !== filter.dynamic_column.column_name); - - return transformedFilter; - } - private buildQueryParams(additionalParams: any = {}): any { - const currentParams = this.route.snapshot.queryParams; - - // Start with pagination parameters - const queryParams: any = { - page_index: 0, - page_size: currentParams.page_size || 30, - ...additionalParams - }; - - // Preserve sort parameters if present - if (currentParams.sort_active) { - queryParams.sort_active = currentParams.sort_active; - } - - if (currentParams.sort_direction) { - queryParams.sort_direction = currentParams.sort_direction; - } - - return queryParams; - } - - selectFiltersSet(selectedFilterSetId: string): void { - console.log('selectFiltersSet ID:', selectedFilterSetId); - if (this.selectedFilterSetId === selectedFilterSetId) { - this.selectedFilterSetId = null; - this.filterSelected.emit(null); - this.shouldAutofocus = false; - const queryParams = this.buildQueryParams(); - this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); - } else { - this.selectedFilterSetId = selectedFilterSetId; - this.shouldAutofocus = true; - const selectedFilter = this.savedFilterMap[selectedFilterSetId]; - this.filterSelected.emit(selectedFilter); - - const additionalParams: any = { - filters: JsonURL.stringify(selectedFilter.filters), - saved_filter: selectedFilterSetId - }; - - if (selectedFilter.dynamicColumn) { - additionalParams.dynamic_column = JsonURL.stringify({ - column_name: selectedFilter.dynamicColumn.column, - comparator: selectedFilter.dynamicColumn.operator - }); - } - - const queryParams = this.buildQueryParams(additionalParams); - this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); - - // Reset autofocus after the component has been rendered - setTimeout(() => { - this.shouldAutofocus = false; - }, 500); - } - } - - selectFilter(entry: { column: string; operator: string; value: any }) { - this.selectedFilter = entry; - } - - getOperatorIcon(operator: string): string { - const iconMap: { [key: string]: string } = { - 'startswith': 'play_arrow', - 'endswith': 'play_arrow', - 'eq': 'drag_handle', - 'contains': 'search', - 'icontains': 'block', - 'empty': 'space_bar', - 'gt': 'keyboard_arrow_right', - 'lt': 'keyboard_arrow_left', - 'gte': 'keyboard_double_arrow_right', - 'lte': 'keyboard_double_arrow_left' - }; - return iconMap[operator] || 'drag_handle'; - } - - getFilter(activeFilter: {column: string, operator: string, value: any}) { - const displayedName = normalizeTableName(activeFilter.column); - const comparator = activeFilter.operator; - const filterValue = activeFilter.value; - if (comparator === 'startswith') { - return `${displayedName} = ${filterValue}...` - } else if (comparator === 'endswith') { - return `${displayedName} = ...${filterValue}` - } else if (comparator === 'contains') { - return `${displayedName} = ...${filterValue}...` - } else if (comparator === 'icontains') { - return `${displayedName} != ...${filterValue}...` - } else if (comparator === 'empty') { - return `${displayedName} = ' '` - } else { - return `${displayedName} ${this.displayedComparators[comparator]} ${filterValue}` - } - } - - get inputs() { - return filterTypes[this._connections.currentConnection.type]; - } - - isWidget(columnName: string) { - return this.tableWidgetsList.includes(columnName); - } - - getInputType(field: string) { - let widgetType; - if (this.isWidget(field)) { - widgetType = this.UIwidgets[this.tableWidgets[field].widget_type]?.type; - } else { - widgetType = this.inputs[this.tableTypes[field]]?.type; - } - return widgetType; - } - - getComparatorType(typeOfComponent) { - if (typeOfComponent === 'text') { - return 'text'; - } else if (typeOfComponent === 'number' || typeOfComponent === 'datetime') { - return 'number'; - } else { - return 'nonComparable'; - } - } - - setWidgets(widgets: any[]) { - this.tableWidgetsList = widgets.map((widget: any) => widget.field_name); - this.tableWidgets = Object.assign({}, ...widgets - .map((widget: any) => { - let params; - if (widget.widget_params !== '// No settings required') { - try { - params = JSON.parse(widget.widget_params); - } catch (_e) { - params = ''; - } - } else { - params = ''; - } - return { - [widget.field_name]: {...widget, widget_params: params} - }; - }) - ); - } - - trackByFn(_index: number, item: any) { - return item.key; - } - - updateDynamicColumnComparator = (comparator: string) => { - if (!this.savedFilterMap) { - console.error('savedFilterMap is undefined'); - return; - } - - if (!this.selectedFilterSetId) { - console.error('selectedFilterSetId is null or undefined'); - return; - } - - const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; - if (!selectedFilter) { - console.error(`No filter found with ID ${this.selectedFilterSetId} in savedFilterMap`); - return; - } - - if (!selectedFilter.dynamicColumn) { - console.error('dynamicColumn is null or undefined for the selected filter'); - return; - } - - selectedFilter.dynamicColumn.operator = comparator; - - if (comparator === 'empty') { - selectedFilter.dynamicColumn.value = ''; - } - } - - updateDynamicColumnValue = (value: any) => { - if (!this.savedFilterMap) { - console.error('savedFilterMap is undefined'); - return; - } - - if (!this.selectedFilterSetId) { - console.error('selectedFilterSetId is null or undefined'); - return; - } - - const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; - if (!selectedFilter) { - console.error(`No filter found with ID ${this.selectedFilterSetId} in savedFilterMap`); - return; - } - - if (!selectedFilter.dynamicColumn) { - console.error('dynamicColumn is null or undefined for the selected filter'); - return; - } - - console.log(value, 'value in updateDynamicColumnValue'); - - selectedFilter.dynamicColumn.value = value; - - if (this.dynamicColumnValueDebounceTimer) { - clearTimeout(this.dynamicColumnValueDebounceTimer); - } - - this.dynamicColumnValueDebounceTimer = setTimeout(() => { - this.applyDynamicColumnChanges(); - }, 800); - } - - applyDynamicColumnChanges() { - if (!this.selectedFilterSetId) return; - - const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; - - console.log('Applying dynamic column changes for filter selectedFilter:', selectedFilter); - - if (!selectedFilter || !selectedFilter.dynamicColumn) return; - - const dynamicColumn = { - column_name: selectedFilter.dynamicColumn.column, - comparator: selectedFilter.dynamicColumn.operator - }; - - const filterValue = selectedFilter.dynamicColumn.value === '' || selectedFilter.dynamicColumn.value === undefined ? null : selectedFilter.dynamicColumn.value; - - const filters = { ...selectedFilter.filters }; - - if (selectedFilter.dynamicColumn.column && - selectedFilter.dynamicColumn.operator && - filterValue !== null) { - filters[selectedFilter.dynamicColumn.column] = { - [selectedFilter.dynamicColumn.operator]: filterValue - }; - } else { - if (filters[selectedFilter.dynamicColumn.column]) { - delete filters[selectedFilter.dynamicColumn.column]; - } - } - - console.log('applyDynamicColumnChanges, filters:', filters); - - // Build filter-related params using the helper method - const additionalParams: any = { - filters: JsonURL.stringify(filters), - dynamic_column: JsonURL.stringify(dynamicColumn), - saved_filter: this.selectedFilterSetId - }; - - const queryParams = this.buildQueryParams(additionalParams); - - const updatedFilterData = { - ...selectedFilter, - filters: filters, - dynamic_column: dynamicColumn - }; + protected posthog = posthog; + @Input() connectionID: string; + @Input() selectedTableName: string; + @Input() selectedTableDisplayName: string; + @Input() tableTypes: any; + @Input() structure: any; + @Input() tableForeignKeys: TableForeignKey[] = []; + @Input() tableWidgets: any = {}; + // @Input() savedFilterData: any; + @Output() filterSelected = new EventEmitter(); + @Input() resetSelection: boolean = false; + + @Input() accessLevel: AccessLevel; + + private dynamicColumnValueDebounceTimer: any = null; + + public savedFilterData = null; + public savedFilterMap: { [key: string]: any } = {}; + + public selectedFilterSetId: string | null = null; + public selectedFilter: any = null; + public shouldAutofocus: boolean = false; + public currentFilterForMenu: any = null; + + public tableStructure: any = null; + public tableRowFieldsShown: { [key: string]: any } = {}; + public tableRowStructure: { [key: string]: any } = {}; + public tableWidgetsList: string[] = []; + public UIwidgets = UIwidgets; + + public displayedComparators = { + eq: '=', + gt: '>', + lt: '<', + gte: '>=', + lte: '<=', + startswith: 'starts with', + endswith: 'ends with', + contains: 'contains', + icontains: 'not contains', + empty: 'is empty', + }; + + constructor( + private dialog: MatDialog, + private _tables: TablesService, + private _connections: ConnectionsService, + private router: Router, + private route: ActivatedRoute, + ) {} + + ngOnInit() { + this.route.paramMap.subscribe((params) => { + const tableNameFromUrl = params.get('table-name'); + if (tableNameFromUrl) { + this.savedFilterData = null; + this.selectedFilterSetId = null; + this.selectedFilter = null; + this.selectedTableName = tableNameFromUrl; + this.loadSavedFilters(); + } + }); + + this.route.queryParamMap.subscribe((params) => { + const savedFilterId = params.get('saved_filter'); + if (savedFilterId) { + this.selectedFilterSetId = savedFilterId; + } else { + this.selectedFilterSetId = null; + } + }); + + this._tables.cast.subscribe((arg) => { + if (arg === 'filters set saved') { + this.loadSavedFilters(); + } + + if (arg === 'filters set updated') { + // Get the current saved filter ID from URL + const savedFilterIdFromUrl = this.route.snapshot.queryParams.saved_filter; + + if (savedFilterIdFromUrl) { + // If we have a filter selected in URL, get latest data and update URL + this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ + next: (data) => { + if (data) { + // Find the updated filter in the response + const updatedFilter = data.find((filter) => filter.id === savedFilterIdFromUrl); + if (updatedFilter) { + const processedFilter = this.processFiltersData(updatedFilter); + + // Update local state + this.selectedFilterSetId = savedFilterIdFromUrl; + this.filterSelected.emit(processedFilter); + + // Update URL with the refreshed filter data + const additionalParams: any = { + filters: JsonURL.stringify(updatedFilter.filters), + saved_filter: savedFilterIdFromUrl, + }; + + if (updatedFilter.dynamic_column) { + additionalParams.dynamic_column = JsonURL.stringify({ + column_name: updatedFilter.dynamic_column.column_name, + comparator: updatedFilter.dynamic_column.comparator, + }); + } + + const queryParams = this.buildQueryParams(additionalParams); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + } + + // Update the full filters map + this.savedFilterData = data.sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), + ); + this.savedFilterMap = Object.assign( + {}, + ...data.map((filter) => { + const transformedFilter = this.processFiltersData(filter); + return { [filter.id]: transformedFilter }; + }), + ); + } + }, + error: (error) => { + console.error('Error fetching updated filters:', error); + this.loadSavedFilters(); + }, + }); + } else { + // Just refresh filters if no filter selected in URL + this.loadSavedFilters(); + } + } + + if (arg === 'delete saved filters') { + this.loadSavedFilters(); + this.selectedFilterSetId = null; + this.filterSelected.emit(null); + const queryParams = this.buildQueryParams(); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + } + }); + + this.tableRowStructure = Object.assign( + {}, + ...this.structure.map((field: TableField) => { + return { [field.column_name]: field }; + }), + ); + } + + loadSavedFilters() { + if (!this.connectionID || !this.selectedTableName) { + return; + } + + this._tables.getSavedFilters(this.connectionID, this.selectedTableName).subscribe({ + next: (data) => { + if (data) { + this.savedFilterData = data.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); + this.savedFilterMap = Object.assign( + {}, + ...data.map((filter) => { + const transformedFilter = this.processFiltersData(filter); + return { [filter.id]: transformedFilter }; + }), + ); + + const params = this.route.snapshot.queryParams; + const dynamicColumn = params.dynamic_column ? JsonURL.parse(params.dynamic_column) : null; + + if (this.selectedFilterSetId && this.savedFilterData.length > 0) { + if (dynamicColumn && this.savedFilterMap[this.selectedFilterSetId]) { + const filters = params.filters ? JsonURL.parse(params.filters) : {}; + + this.savedFilterMap[this.selectedFilterSetId].dynamicColumn = { + column: dynamicColumn.column_name, + operator: dynamicColumn.comparator, + value: filters[dynamicColumn.column_name]?.[dynamicColumn.comparator] || null, + }; + } + } + } + }, + error: (error) => { + console.error('Error fetching saved filters:', error); + }, + }); + } + + ngOnChanges() { + if (this.resetSelection) { + this.selectedFilterSetId = null; + } + } + + ngOnDestroy() { + if (this.dynamicColumnValueDebounceTimer) { + clearTimeout(this.dynamicColumnValueDebounceTimer); + } + } + + handleOpenSavedFiltersDialog(filtersSet: any = null) { + const _dialogRef = this.dialog.open(SavedFiltersDialogComponent, { + width: '56em', + data: { + connectionID: this.connectionID, + tableName: this.selectedTableName, + displayTableName: this.selectedTableDisplayName, + structure: this.structure, + tableForeignKeys: this.tableForeignKeys, + tableWidgets: this.tableWidgets, + filtersSet: filtersSet + ? filtersSet + : { + name: '', + filters: {}, + }, + }, + }); + + // No need to handle URL updates here - it's now handled in the tables.cast subscription + // when 'filters set updated' is received + } + + setCurrentFilter(filter: any) { + this.currentFilterForMenu = filter; + } + + handleEditFilter(filter: any) { + if (filter) { + this.handleOpenSavedFiltersDialog(filter); + } + } + + handleDeleteFilter(filter: any) { + if (filter) { + this._tables.deleteSavedFilter(this.connectionID, this.selectedTableName, filter.id).subscribe({ + next: () => { + // The deletion will trigger 'delete saved filters' event which will refresh the list + }, + error: (error) => { + console.error('Error deleting filter:', error); + }, + }); + } + } + + getFilterEntries(filters: any): { column: string; operator: string; value: string }[] { + if (!filters) return []; + + const entries: { column: string; operator: string; value: string }[] = []; + + Object.keys(filters).forEach((column) => { + const operations = filters[column]; + Object.keys(operations).forEach((operator) => { + entries.push({ + column, + operator, + value: operations[operator], + }); + }); + }); + return entries; + } + + processFiltersData(filter: any) { + const transformedFilter = { + ...filter, + filterEntries: this.getFilterEntries(filter.filters), + staticFilters: [] as { column: string; operator: string; value: any }[], + dynamicColumn: null as { column: string; operator: string; value: any } | null, + }; + + if (filter.dynamic_column?.column_name) { + transformedFilter.dynamicColumn = { + column: filter.dynamic_column.column_name, + operator: filter.dynamic_column.comparator, + value: null, + }; + + const dynamicColFilters = filter.filters?.[filter.dynamic_column.column_name]; + if (dynamicColFilters) { + const operator = filter.dynamic_column.comparator; + transformedFilter.dynamicColumn.value = dynamicColFilters[operator]; + } + } + + transformedFilter.staticFilters = this.getFilterEntries(filter.filters).filter( + (entry) => !filter.dynamic_column || entry.column !== filter.dynamic_column.column_name, + ); + + return transformedFilter; + } + private buildQueryParams(additionalParams: any = {}): any { + const currentParams = this.route.snapshot.queryParams; + + // Start with pagination parameters + const queryParams: any = { + page_index: 0, + page_size: currentParams.page_size || 30, + ...additionalParams, + }; + + // Preserve sort parameters if present + if (currentParams.sort_active) { + queryParams.sort_active = currentParams.sort_active; + } + + if (currentParams.sort_direction) { + queryParams.sort_direction = currentParams.sort_direction; + } + + return queryParams; + } + + selectFiltersSet(selectedFilterSetId: string): void { + console.log('selectFiltersSet ID:', selectedFilterSetId); + if (this.selectedFilterSetId === selectedFilterSetId) { + this.selectedFilterSetId = null; + this.filterSelected.emit(null); + this.shouldAutofocus = false; + const queryParams = this.buildQueryParams(); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + } else { + this.selectedFilterSetId = selectedFilterSetId; + this.shouldAutofocus = true; + const selectedFilter = this.savedFilterMap[selectedFilterSetId]; + this.filterSelected.emit(selectedFilter); + + const additionalParams: any = { + filters: JsonURL.stringify(selectedFilter.filters), + saved_filter: selectedFilterSetId, + }; + + if (selectedFilter.dynamicColumn) { + additionalParams.dynamic_column = JsonURL.stringify({ + column_name: selectedFilter.dynamicColumn.column, + comparator: selectedFilter.dynamicColumn.operator, + }); + } + + const queryParams = this.buildQueryParams(additionalParams); + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { queryParams }); + + // Reset autofocus after the component has been rendered + setTimeout(() => { + this.shouldAutofocus = false; + }, 500); + } + } + + selectFilter(entry: { column: string; operator: string; value: any }) { + this.selectedFilter = entry; + } + + getOperatorIcon(operator: string): string { + const iconMap: { [key: string]: string } = { + startswith: 'play_arrow', + endswith: 'play_arrow', + eq: 'drag_handle', + contains: 'search', + icontains: 'block', + empty: 'space_bar', + gt: 'keyboard_arrow_right', + lt: 'keyboard_arrow_left', + gte: 'keyboard_double_arrow_right', + lte: 'keyboard_double_arrow_left', + }; + return iconMap[operator] || 'drag_handle'; + } + + getFilter(activeFilter: { column: string; operator: string; value: any }) { + const displayedName = normalizeTableName(activeFilter.column); + const comparator = activeFilter.operator; + const filterValue = activeFilter.value; + if (comparator === 'startswith') { + return `${displayedName} = ${filterValue}...`; + } else if (comparator === 'endswith') { + return `${displayedName} = ...${filterValue}`; + } else if (comparator === 'contains') { + return `${displayedName} = ...${filterValue}...`; + } else if (comparator === 'icontains') { + return `${displayedName} != ...${filterValue}...`; + } else if (comparator === 'empty') { + return `${displayedName} = ' '`; + } else { + return `${displayedName} ${this.displayedComparators[comparator]} ${filterValue}`; + } + } + + get inputs() { + return filterTypes[this._connections.currentConnection.type]; + } + + isWidget(columnName: string) { + return this.tableWidgetsList.includes(columnName); + } + + getInputType(field: string) { + let widgetType; + if (this.isWidget(field)) { + widgetType = this.UIwidgets[this.tableWidgets[field].widget_type]?.type; + } else { + widgetType = this.inputs[this.tableTypes[field]]?.type; + } + return widgetType; + } + + getComparatorType(typeOfComponent) { + if (typeOfComponent === 'text') { + return 'text'; + } else if (typeOfComponent === 'number' || typeOfComponent === 'datetime') { + return 'number'; + } else { + return 'nonComparable'; + } + } + + setWidgets(widgets: any[]) { + this.tableWidgetsList = widgets.map((widget: any) => widget.field_name); + this.tableWidgets = Object.assign( + {}, + ...widgets.map((widget: any) => { + let params; + if (widget.widget_params !== '// No settings required') { + try { + params = JSON.parse(widget.widget_params); + } catch (_e) { + params = ''; + } + } else { + params = ''; + } + return { + [widget.field_name]: { ...widget, widget_params: params }, + }; + }), + ); + } + + trackByFn(_index: number, item: any) { + return item.key; + } + + updateDynamicColumnComparator = (comparator: string) => { + if (!this.savedFilterMap) { + console.error('savedFilterMap is undefined'); + return; + } + + if (!this.selectedFilterSetId) { + console.error('selectedFilterSetId is null or undefined'); + return; + } + + const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; + if (!selectedFilter) { + console.error(`No filter found with ID ${this.selectedFilterSetId} in savedFilterMap`); + return; + } + + if (!selectedFilter.dynamicColumn) { + console.error('dynamicColumn is null or undefined for the selected filter'); + return; + } + + selectedFilter.dynamicColumn.operator = comparator; + + if (comparator === 'empty') { + selectedFilter.dynamicColumn.value = ''; + } + }; + + updateDynamicColumnValue = (value: any) => { + if (!this.savedFilterMap) { + console.error('savedFilterMap is undefined'); + return; + } + + if (!this.selectedFilterSetId) { + console.error('selectedFilterSetId is null or undefined'); + return; + } + + const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; + if (!selectedFilter) { + console.error(`No filter found with ID ${this.selectedFilterSetId} in savedFilterMap`); + return; + } + + if (!selectedFilter.dynamicColumn) { + console.error('dynamicColumn is null or undefined for the selected filter'); + return; + } + + console.log(value, 'value in updateDynamicColumnValue'); + + selectedFilter.dynamicColumn.value = value; + + if (this.dynamicColumnValueDebounceTimer) { + clearTimeout(this.dynamicColumnValueDebounceTimer); + } + + this.dynamicColumnValueDebounceTimer = setTimeout(() => { + this.applyDynamicColumnChanges(); + }, 800); + }; + + applyDynamicColumnChanges() { + if (!this.selectedFilterSetId) return; + + const selectedFilter = this.savedFilterMap[this.selectedFilterSetId]; + + console.log('Applying dynamic column changes for filter selectedFilter:', selectedFilter); + + if (!selectedFilter || !selectedFilter.dynamicColumn) return; + + const dynamicColumn = { + column_name: selectedFilter.dynamicColumn.column, + comparator: selectedFilter.dynamicColumn.operator, + }; + + const filterValue = + selectedFilter.dynamicColumn.value === '' || selectedFilter.dynamicColumn.value === undefined + ? null + : selectedFilter.dynamicColumn.value; + + const filters = { ...selectedFilter.filters }; + + if (selectedFilter.dynamicColumn.column && selectedFilter.dynamicColumn.operator && filterValue !== null) { + filters[selectedFilter.dynamicColumn.column] = { + [selectedFilter.dynamicColumn.operator]: filterValue, + }; + } else { + if (filters[selectedFilter.dynamicColumn.column]) { + delete filters[selectedFilter.dynamicColumn.column]; + } + } + + console.log('applyDynamicColumnChanges, filters:', filters); + + // Build filter-related params using the helper method + const additionalParams: any = { + filters: JsonURL.stringify(filters), + dynamic_column: JsonURL.stringify(dynamicColumn), + saved_filter: this.selectedFilterSetId, + }; + + const queryParams = this.buildQueryParams(additionalParams); - updatedFilterData.filterEntries = this.getFilterEntries(filters); + const updatedFilterData = { + ...selectedFilter, + filters: filters, + dynamic_column: dynamicColumn, + }; + + updatedFilterData.filterEntries = this.getFilterEntries(filters); - this.filterSelected.emit(updatedFilterData); + this.filterSelected.emit(updatedFilterData); - this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { - queryParams - }); - } + this.router.navigate([`/dashboard/${this.connectionID}/${this.selectedTableName}`], { + queryParams, + }); + } } diff --git a/frontend/src/app/components/dashboards/dashboard-delete-dialog/dashboard-delete-dialog.component.ts b/frontend/src/app/components/dashboards/dashboard-delete-dialog/dashboard-delete-dialog.component.ts index fef945f50..39447c592 100644 --- a/frontend/src/app/components/dashboards/dashboard-delete-dialog/dashboard-delete-dialog.component.ts +++ b/frontend/src/app/components/dashboards/dashboard-delete-dialog/dashboard-delete-dialog.component.ts @@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { Dashboard } from 'src/app/models/dashboard'; import { DashboardsService } from 'src/app/services/dashboards.service'; @@ -30,6 +31,7 @@ export class DashboardDeleteDialogComponent { this.angulartics2.eventTrack.next({ action: 'Dashboards: dashboard deleted successfully', }); + posthog.capture('Dashboards: dashboard deleted successfully'); this.dialogRef.close(true); } this.submitting.set(false); diff --git a/frontend/src/app/components/dashboards/dashboard-edit-dialog/dashboard-edit-dialog.component.ts b/frontend/src/app/components/dashboards/dashboard-edit-dialog/dashboard-edit-dialog.component.ts index 6ccd14a0c..96e7607af 100644 --- a/frontend/src/app/components/dashboards/dashboard-edit-dialog/dashboard-edit-dialog.component.ts +++ b/frontend/src/app/components/dashboards/dashboard-edit-dialog/dashboard-edit-dialog.component.ts @@ -7,6 +7,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { Dashboard } from 'src/app/models/dashboard'; import { DashboardsService } from 'src/app/services/dashboards.service'; @@ -58,6 +59,7 @@ export class DashboardEditDialogComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: dashboard updated successfully', }); + posthog.capture('Dashboards: dashboard updated successfully'); this.dialogRef.close(true); } } else { @@ -66,6 +68,7 @@ export class DashboardEditDialogComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: dashboard created successfully', }); + posthog.capture('Dashboards: dashboard created successfully'); this.dialogRef.close(true); } } diff --git a/frontend/src/app/components/dashboards/dashboard-view/dashboard-view.component.ts b/frontend/src/app/components/dashboards/dashboard-view/dashboard-view.component.ts index a8494e401..58e99fd61 100644 --- a/frontend/src/app/components/dashboards/dashboard-view/dashboard-view.component.ts +++ b/frontend/src/app/components/dashboards/dashboard-view/dashboard-view.component.ts @@ -20,6 +20,7 @@ import { GridType, } from 'angular-gridster2'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { DashboardWidget } from 'src/app/models/dashboard'; import { ConnectionsService } from 'src/app/services/connections.service'; import { DashboardsService } from 'src/app/services/dashboards.service'; @@ -177,6 +178,7 @@ export class DashboardViewComponent implements OnInit { this.angulartics2.eventTrack.next({ action: `Dashboards: edit mode ${this.editMode() ? 'enabled' : 'disabled'}`, }); + posthog.capture(`Dashboards: edit mode ${this.editMode() ? 'enabled' : 'disabled'}`); } async openAddWidgetDialog(): Promise { @@ -191,6 +193,7 @@ export class DashboardViewComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: add widget dialog opened', }); + posthog.capture('Dashboards: add widget dialog opened'); const result = await dialogRef.afterClosed().toPromise(); if (result) { @@ -210,6 +213,7 @@ export class DashboardViewComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: edit widget dialog opened', }); + posthog.capture('Dashboards: edit widget dialog opened'); const result = await dialogRef.afterClosed().toPromise(); if (result) { @@ -229,6 +233,7 @@ export class DashboardViewComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: delete widget dialog opened', }); + posthog.capture('Dashboards: delete widget dialog opened'); const result = await dialogRef.afterClosed().toPromise(); if (result) { diff --git a/frontend/src/app/components/dashboards/dashboards-list/dashboards-list.component.ts b/frontend/src/app/components/dashboards/dashboards-list/dashboards-list.component.ts index dd5a851e8..bf63764c8 100644 --- a/frontend/src/app/components/dashboards/dashboards-list/dashboards-list.component.ts +++ b/frontend/src/app/components/dashboards/dashboards-list/dashboards-list.component.ts @@ -14,6 +14,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { Dashboard } from 'src/app/models/dashboard'; import { ConnectionsService } from 'src/app/services/connections.service'; import { DashboardsService } from 'src/app/services/dashboards.service'; @@ -107,6 +108,7 @@ export class DashboardsListComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: view dashboard opened', }); + posthog.capture('Dashboards: view dashboard opened'); } openCreateDialog(): void { @@ -117,6 +119,7 @@ export class DashboardsListComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: create dashboard dialog opened', }); + posthog.capture('Dashboards: create dashboard dialog opened'); } openEditDialog(dashboard: Dashboard): void { @@ -127,6 +130,7 @@ export class DashboardsListComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: edit dashboard dialog opened', }); + posthog.capture('Dashboards: edit dashboard dialog opened'); } openDeleteDialog(dashboard: Dashboard): void { @@ -137,5 +141,6 @@ export class DashboardsListComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: delete dashboard dialog opened', }); + posthog.capture('Dashboards: delete dashboard dialog opened'); } } diff --git a/frontend/src/app/components/dashboards/widget-delete-dialog/widget-delete-dialog.component.ts b/frontend/src/app/components/dashboards/widget-delete-dialog/widget-delete-dialog.component.ts index ef82d2255..a4a388a60 100644 --- a/frontend/src/app/components/dashboards/widget-delete-dialog/widget-delete-dialog.component.ts +++ b/frontend/src/app/components/dashboards/widget-delete-dialog/widget-delete-dialog.component.ts @@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { DashboardWidget } from 'src/app/models/dashboard'; import { DashboardsService } from 'src/app/services/dashboards.service'; @@ -35,6 +36,7 @@ export class WidgetDeleteDialogComponent { this.angulartics2.eventTrack.next({ action: 'Dashboards: widget deleted successfully', }); + posthog.capture('Dashboards: widget deleted successfully'); this.dialogRef.close(true); } this.submitting.set(false); diff --git a/frontend/src/app/components/dashboards/widget-edit-dialog/widget-edit-dialog.component.ts b/frontend/src/app/components/dashboards/widget-edit-dialog/widget-edit-dialog.component.ts index d50adc11e..1ed9106f3 100644 --- a/frontend/src/app/components/dashboards/widget-edit-dialog/widget-edit-dialog.component.ts +++ b/frontend/src/app/components/dashboards/widget-edit-dialog/widget-edit-dialog.component.ts @@ -3,12 +3,13 @@ import { Component, computed, Inject, OnInit, signal } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; import { MatDividerModule } from '@angular/material/divider'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatSelectModule } from '@angular/material/select'; import { Router, RouterModule } from '@angular/router'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { DashboardWidget } from 'src/app/models/dashboard'; import { DashboardsService } from 'src/app/services/dashboards.service'; import { SavedQueriesService } from 'src/app/services/saved-queries.service'; @@ -80,6 +81,7 @@ export class WidgetEditDialogComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: widget updated successfully', }); + posthog.capture('Dashboards: widget updated successfully'); this.dialogRef.close(true); } } else { @@ -96,6 +98,7 @@ export class WidgetEditDialogComponent implements OnInit { this.angulartics2.eventTrack.next({ action: 'Dashboards: widget created successfully', }); + posthog.capture('Dashboards: widget created successfully'); this.dialogRef.close(true); } } diff --git a/frontend/src/app/components/login/login.component.html b/frontend/src/app/components/login/login.component.html index 86266b272..304651a26 100644 --- a/frontend/src/app/components/login/login.component.html +++ b/frontend/src/app/components/login/login.component.html @@ -44,6 +44,7 @@

#email="ngModel" required angulartics2On="change" angularticsAction="Login: email is changed" + (change)="posthog.capture('Login: email is changed')" [(ngModel)]="user.email" > Invalid email format. @@ -69,6 +70,7 @@

placeholder="Password" angulartics2On="change" angularticsAction="Login: password is changed" + (change)="posthog.capture('Login: password is changed')" [(ngModel)]="user.password"> @@ -78,14 +80,15 @@

@@ -96,7 +99,7 @@

class="login-form__sso-button" angulartics2On="click" angularticsAction="Login: login with sso is clicked" - (click)="openLoginWithSSOdialog()"> + (click)="openLoginWithSSOdialog(); posthog.capture('Login: login with sso is clicked')"> @@ -118,6 +121,7 @@

class="login-form__login-button" angulartics2On="click" angularticsAction="Login: login is clicked" + (click)="posthog.capture('Login: login is clicked')" [disabled]="submitting || loginForm.form.invalid || loginForm.form.pristine"> {{ submitting ? 'Submitting' : 'Log in'}} diff --git a/frontend/src/app/components/login/login.component.ts b/frontend/src/app/components/login/login.component.ts index 0bfebeef5..cc6b13c75 100644 --- a/frontend/src/app/components/login/login.component.ts +++ b/frontend/src/app/components/login/login.component.ts @@ -11,6 +11,7 @@ import { MatSelectModule } from '@angular/material/select'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; import { accounts } from 'google-one-tap'; +import posthog from 'posthog-js'; import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive'; import { AlertActionType, AlertType } from 'src/app/models/alert'; import { ExistingAuthUser } from 'src/app/models/user'; @@ -44,6 +45,7 @@ declare var google: any; schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class LoginComponent implements OnInit, AfterViewInit { + protected posthog = posthog; public isSaas = (environment as any).saas; public isCustomDomain: boolean = false; public user: ExistingAuthUser = { @@ -101,6 +103,7 @@ export class LoginComponent implements OnInit, AfterViewInit { this.angulartics2.eventTrack.next({ action: 'Login: google login success', }); + posthog.capture('Login: google login success'); }); }); }, @@ -124,6 +127,9 @@ export class LoginComponent implements OnInit, AfterViewInit { count: companies.length, }, }); + posthog.capture('Login: companies is received', { + count: companies.length, + }); this.userCompanies = companies; this.isLoadingUserCompanies = false; @@ -144,11 +150,13 @@ export class LoginComponent implements OnInit, AfterViewInit { this.angulartics2.eventTrack.next({ action: 'Login: login success', }); + posthog.capture('Login: login success'); }, (_error) => { this.angulartics2.eventTrack.next({ action: 'Login: login unsuccessful', }); + posthog.capture('Login: login unsuccessful'); this.submitting = false; }, () => (this.submitting = false), @@ -160,6 +168,7 @@ export class LoginComponent implements OnInit, AfterViewInit { this.angulartics2.eventTrack.next({ action: 'Login: github login redirect', }); + posthog.capture('Login: github login redirect'); } openLoginWithSSOdialog() { @@ -176,11 +185,13 @@ export class LoginComponent implements OnInit, AfterViewInit { this.angulartics2.eventTrack.next({ action: 'Login: login with 2fa success', }); + posthog.capture('Login: login with 2fa success'); }, (_error) => { this.angulartics2.eventTrack.next({ action: 'Login: login with 2fa unsuccessful', }); + posthog.capture('Login: login with 2fa unsuccessful'); this.submitting = false; }, () => (this.submitting = false), diff --git a/frontend/src/app/components/password-change/password-change.component.ts b/frontend/src/app/components/password-change/password-change.component.ts index 9b642daff..2ac4a14b7 100644 --- a/frontend/src/app/components/password-change/password-change.component.ts +++ b/frontend/src/app/components/password-change/password-change.component.ts @@ -1,58 +1,65 @@ -import { CUSTOM_ELEMENTS_SCHEMA, Component, OnInit } from '@angular/core'; - -import { AlertComponent } from '../ui-components/alert/alert.component'; -import { Angulartics2 } from 'angulartics2'; import { CommonModule } from '@angular/common'; +import { Component, CUSTOM_ELEMENTS_SCHEMA, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { Router } from '@angular/router'; +import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; import { User } from 'src/app/models/user'; -import { UserPasswordComponent } from '../ui-components/user-password/user-password.component'; import { UserService } from 'src/app/services/user.service'; -import { MatButtonModule } from '@angular/material/button'; +import { AlertComponent } from '../ui-components/alert/alert.component'; +import { UserPasswordComponent } from '../ui-components/user-password/user-password.component'; @Component({ - selector: 'app-password-change', - templateUrl: './password-change.component.html', - styleUrls: ['./password-change.component.css'], - imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule, MatButtonModule, AlertComponent, UserPasswordComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + selector: 'app-password-change', + templateUrl: './password-change.component.html', + styleUrls: ['./password-change.component.css'], + imports: [ + CommonModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + AlertComponent, + UserPasswordComponent, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class PasswordChangeComponent implements OnInit { + public currentUser: User = null; + public oldPassword: string = ''; + public newPassword: string = ''; + public submitting: boolean = false; - public currentUser: User = null; - public oldPassword: string = ''; - public newPassword: string = ''; - public submitting: boolean = false; - - constructor( - private _userService: UserService, - public router: Router, - private angulartics2: Angulartics2, - ) { } - - ngOnInit(): void { - this._userService.cast.subscribe(user => this.currentUser = user); - } + constructor( + private _userService: UserService, + public router: Router, + private angulartics2: Angulartics2, + ) {} - updatePasswordField(updatedValue: string) { - this.newPassword = updatedValue; - } + ngOnInit(): void { + this._userService.cast.subscribe((user) => (this.currentUser = user)); + } - updatePassword() { - this.submitting = true; - this._userService.changePassword(this.oldPassword, this.newPassword, this.currentUser.email) - .subscribe(() => { - this.submitting = false; - this.angulartics2.eventTrack.next({ - action: 'Manual password change: password updated successfully', - }); - this.router.navigate(['/user-settings']); - }, - undefined, - () => this.submitting = false - ); - } + updatePasswordField(updatedValue: string) { + this.newPassword = updatedValue; + } + updatePassword() { + this.submitting = true; + this._userService.changePassword(this.oldPassword, this.newPassword, this.currentUser.email).subscribe( + () => { + this.submitting = false; + this.angulartics2.eventTrack.next({ + action: 'Manual password change: password updated successfully', + }); + posthog.capture('Manual password change: password updated successfully'); + this.router.navigate(['/user-settings']); + }, + undefined, + () => (this.submitting = false), + ); + } } diff --git a/frontend/src/app/components/registration/registration.component.html b/frontend/src/app/components/registration/registration.component.html index 294c98871..960b8741f 100644 --- a/frontend/src/app/components/registration/registration.component.html +++ b/frontend/src/app/components/registration/registration.component.html @@ -16,14 +16,15 @@

+ angularticsAction="Reg: sign up with google" + (click)="posthog.capture('Reg: sign up with google')">
@@ -35,6 +36,7 @@

Invalid email format. @@ -54,6 +56,7 @@

diff --git a/frontend/src/app/components/registration/registration.component.ts b/frontend/src/app/components/registration/registration.component.ts index fb5863bb6..7a24e964a 100644 --- a/frontend/src/app/components/registration/registration.component.ts +++ b/frontend/src/app/components/registration/registration.component.ts @@ -7,6 +7,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { Router } from '@angular/router'; import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; +import posthog from 'posthog-js'; import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive'; import { AlertActionType, AlertType } from 'src/app/models/alert'; import { NewAuthUser } from 'src/app/models/user'; @@ -37,6 +38,7 @@ import { UserPasswordComponent } from '../ui-components/user-password/user-passw schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class RegistrationComponent implements OnInit, AfterViewInit { + protected posthog = posthog; @ViewChild(TurnstileComponent) turnstileWidget: TurnstileComponent; public isSaas = (environment as any).saas; @@ -64,6 +66,7 @@ export class RegistrationComponent implements OnInit, AfterViewInit { this.angulartics2.eventTrack.next({ action: 'Reg: Registration page (component) is loaded', }); + posthog.capture('Reg: Registration page (component) is loaded'); const error = new URLSearchParams(location.search).get('error'); if (error) @@ -89,6 +92,7 @@ export class RegistrationComponent implements OnInit, AfterViewInit { this.angulartics2.eventTrack.next({ action: 'Reg: google register success', }); + posthog.capture('Reg: google register success'); }); }); }, @@ -121,11 +125,13 @@ export class RegistrationComponent implements OnInit, AfterViewInit { this.angulartics2.eventTrack.next({ action: 'Reg: sing up success', }); + posthog.capture('Reg: sing up success'); }, (_error) => { this.angulartics2.eventTrack.next({ action: 'Reg: sing up unsuccessful', }); + posthog.capture('Reg: sing up unsuccessful'); this.submitting = false; this._resetTurnstile(); }, @@ -157,5 +163,6 @@ export class RegistrationComponent implements OnInit, AfterViewInit { this.angulartics2.eventTrack.next({ action: 'Reg: github register redirect', }); + posthog.capture('Reg: github register redirect'); } } diff --git a/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.ts b/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.ts index e8fc03127..3f0052e39 100644 --- a/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.ts +++ b/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.ts @@ -1,130 +1,127 @@ -import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { MatDialogRef, MatDialogModule } from '@angular/material/dialog'; +import { Component } from '@angular/core'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatInputModule } from '@angular/material/input'; -import { MatFormFieldModule } from '@angular/material/form-field'; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDatepickerModule } from '@angular/material/datepicker'; import { provideNativeDateAdapter } from '@angular/material/core'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; import { MatTooltipModule } from '@angular/material/tooltip'; import { Angulartics2 } from 'angulartics2'; - +import posthog from 'posthog-js'; import { SecretsService } from 'src/app/services/secrets.service'; @Component({ - selector: 'app-create-secret-dialog', - templateUrl: './create-secret-dialog.component.html', - styleUrls: ['./create-secret-dialog.component.css'], - providers: [provideNativeDateAdapter()], - imports: [ - CommonModule, - FormsModule, - ReactiveFormsModule, - MatDialogModule, - MatButtonModule, - MatInputModule, - MatFormFieldModule, - MatCheckboxModule, - MatDatepickerModule, - MatIconModule, - MatTooltipModule, - ] + selector: 'app-create-secret-dialog', + templateUrl: './create-secret-dialog.component.html', + styleUrls: ['./create-secret-dialog.component.css'], + providers: [provideNativeDateAdapter()], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + MatDialogModule, + MatButtonModule, + MatInputModule, + MatFormFieldModule, + MatCheckboxModule, + MatDatepickerModule, + MatIconModule, + MatTooltipModule, + ], }) export class CreateSecretDialogComponent { - public form: FormGroup; - public submitting = false; - public showValue = false; - public showMasterPassword = false; - public minDate = new Date(); + public form: FormGroup; + public submitting = false; + public showValue = false; + public showMasterPassword = false; + public minDate = new Date(); - constructor( - private fb: FormBuilder, - private dialogRef: MatDialogRef, - private _secrets: SecretsService, - private angulartics2: Angulartics2 - ) { - this.form = this.fb.group({ - slug: ['', [ - Validators.required, - Validators.maxLength(255), - Validators.pattern(/^[a-zA-Z0-9_-]+$/) - ]], - value: ['', [Validators.required, Validators.maxLength(10000)]], - expiresAt: [null], - masterEncryption: [false], - masterPassword: [''], - }); + constructor( + private fb: FormBuilder, + private dialogRef: MatDialogRef, + private _secrets: SecretsService, + private angulartics2: Angulartics2, + ) { + this.form = this.fb.group({ + slug: ['', [Validators.required, Validators.maxLength(255), Validators.pattern(/^[a-zA-Z0-9_-]+$/)]], + value: ['', [Validators.required, Validators.maxLength(10000)]], + expiresAt: [null], + masterEncryption: [false], + masterPassword: [''], + }); - this.form.get('masterEncryption')?.valueChanges.subscribe(enabled => { - const masterPasswordControl = this.form.get('masterPassword'); - if (enabled) { - masterPasswordControl?.setValidators([Validators.required, Validators.minLength(8)]); - } else { - masterPasswordControl?.clearValidators(); - masterPasswordControl?.setValue(''); - } - masterPasswordControl?.updateValueAndValidity(); - }); - } + this.form.get('masterEncryption')?.valueChanges.subscribe((enabled) => { + const masterPasswordControl = this.form.get('masterPassword'); + if (enabled) { + masterPasswordControl?.setValidators([Validators.required, Validators.minLength(8)]); + } else { + masterPasswordControl?.clearValidators(); + masterPasswordControl?.setValue(''); + } + masterPasswordControl?.updateValueAndValidity(); + }); + } - get slugError(): string { - const control = this.form.get('slug'); - if (control?.hasError('required')) return 'Unique identifier is required'; - if (control?.hasError('maxlength')) return 'Unique identifier must be 255 characters or less'; - if (control?.hasError('pattern')) return 'Only letters, numbers, hyphens, and underscores allowed (no spaces)'; - return ''; - } + get slugError(): string { + const control = this.form.get('slug'); + if (control?.hasError('required')) return 'Unique identifier is required'; + if (control?.hasError('maxlength')) return 'Unique identifier must be 255 characters or less'; + if (control?.hasError('pattern')) return 'Only letters, numbers, hyphens, and underscores allowed (no spaces)'; + return ''; + } - get valueError(): string { - const control = this.form.get('value'); - if (control?.hasError('required')) return 'Value is required'; - if (control?.hasError('maxlength')) return 'Value must be 10000 characters or less'; - return ''; - } + get valueError(): string { + const control = this.form.get('value'); + if (control?.hasError('required')) return 'Value is required'; + if (control?.hasError('maxlength')) return 'Value must be 10000 characters or less'; + return ''; + } - get masterPasswordError(): string { - const control = this.form.get('masterPassword'); - if (control?.hasError('required')) return 'Master password is required for encryption'; - if (control?.hasError('minlength')) return 'Master password must be at least 8 characters'; - return ''; - } + get masterPasswordError(): string { + const control = this.form.get('masterPassword'); + if (control?.hasError('required')) return 'Master password is required for encryption'; + if (control?.hasError('minlength')) return 'Master password must be at least 8 characters'; + return ''; + } - toggleValueVisibility(): void { - this.showValue = !this.showValue; - } + toggleValueVisibility(): void { + this.showValue = !this.showValue; + } - toggleMasterPasswordVisibility(): void { - this.showMasterPassword = !this.showMasterPassword; - } + toggleMasterPasswordVisibility(): void { + this.showMasterPassword = !this.showMasterPassword; + } - onSubmit(): void { - if (this.form.invalid) return; + onSubmit(): void { + if (this.form.invalid) return; - this.submitting = true; - const formValue = this.form.value; + this.submitting = true; + const formValue = this.form.value; - const payload = { - slug: formValue.slug, - value: formValue.value, - expiresAt: formValue.expiresAt ? new Date(formValue.expiresAt).toISOString() : undefined, - masterEncryption: formValue.masterEncryption || undefined, - masterPassword: formValue.masterEncryption ? formValue.masterPassword : undefined, - }; + const payload = { + slug: formValue.slug, + value: formValue.value, + expiresAt: formValue.expiresAt ? new Date(formValue.expiresAt).toISOString() : undefined, + masterEncryption: formValue.masterEncryption || undefined, + masterPassword: formValue.masterEncryption ? formValue.masterPassword : undefined, + }; - this._secrets.createSecret(payload).subscribe({ - next: () => { - this.angulartics2.eventTrack.next({ - action: 'Secrets: secret created successfully', - }); - this.submitting = false; - this.dialogRef.close(true); - }, - error: () => { - this.submitting = false; - } - }); - } + this._secrets.createSecret(payload).subscribe({ + next: () => { + this.angulartics2.eventTrack.next({ + action: 'Secrets: secret created successfully', + }); + posthog.capture('Secrets: secret created successfully'); + this.submitting = false; + this.dialogRef.close(true); + }, + error: () => { + this.submitting = false; + }, + }); + } } diff --git a/frontend/src/app/components/secrets/delete-secret-dialog/delete-secret-dialog.component.ts b/frontend/src/app/components/secrets/delete-secret-dialog/delete-secret-dialog.component.ts index bab015df6..56ac28c8f 100644 --- a/frontend/src/app/components/secrets/delete-secret-dialog/delete-secret-dialog.component.ts +++ b/frontend/src/app/components/secrets/delete-secret-dialog/delete-secret-dialog.component.ts @@ -1,47 +1,43 @@ -import { Component, Inject } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; +import { Component, Inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { Angulartics2 } from 'angulartics2'; - -import { SecretsService } from 'src/app/services/secrets.service'; +import posthog from 'posthog-js'; import { Secret } from 'src/app/models/secret'; +import { SecretsService } from 'src/app/services/secrets.service'; @Component({ - selector: 'app-delete-secret-dialog', - templateUrl: './delete-secret-dialog.component.html', - styleUrls: ['./delete-secret-dialog.component.css'], - imports: [ - CommonModule, - MatDialogModule, - MatButtonModule, - MatIconModule, - ] + selector: 'app-delete-secret-dialog', + templateUrl: './delete-secret-dialog.component.html', + styleUrls: ['./delete-secret-dialog.component.css'], + imports: [CommonModule, MatDialogModule, MatButtonModule, MatIconModule], }) export class DeleteSecretDialogComponent { - public submitting = false; + public submitting = false; - constructor( - @Inject(MAT_DIALOG_DATA) public data: { secret: Secret }, - private dialogRef: MatDialogRef, - private _secrets: SecretsService, - private angulartics2: Angulartics2 - ) {} + constructor( + @Inject(MAT_DIALOG_DATA) public data: { secret: Secret }, + private dialogRef: MatDialogRef, + private _secrets: SecretsService, + private angulartics2: Angulartics2, + ) {} - onDelete(): void { - this.submitting = true; - this._secrets.deleteSecret(this.data.secret.slug).subscribe({ - next: () => { - this.angulartics2.eventTrack.next({ - action: 'Secrets: secret deleted successfully', - }); - this.submitting = false; - this.dialogRef.close(true); - }, - error: () => { - this.submitting = false; - } - }); - } + onDelete(): void { + this.submitting = true; + this._secrets.deleteSecret(this.data.secret.slug).subscribe({ + next: () => { + this.angulartics2.eventTrack.next({ + action: 'Secrets: secret deleted successfully', + }); + posthog.capture('Secrets: secret deleted successfully'); + this.submitting = false; + this.dialogRef.close(true); + }, + error: () => { + this.submitting = false; + }, + }); + } } diff --git a/frontend/src/app/components/secrets/edit-secret-dialog/edit-secret-dialog.component.ts b/frontend/src/app/components/secrets/edit-secret-dialog/edit-secret-dialog.component.ts index 0bff581eb..9cf895c5c 100644 --- a/frontend/src/app/components/secrets/edit-secret-dialog/edit-secret-dialog.component.ts +++ b/frontend/src/app/components/secrets/edit-secret-dialog/edit-secret-dialog.component.ts @@ -1,122 +1,123 @@ -import { Component, Inject } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; +import { Component, Inject } from '@angular/core'; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; -import { MatInputModule } from '@angular/material/input'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { provideNativeDateAdapter } from '@angular/material/core'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; -import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatInputModule } from '@angular/material/input'; import { MatTooltipModule } from '@angular/material/tooltip'; import { Angulartics2 } from 'angulartics2'; - -import { SecretsService } from 'src/app/services/secrets.service'; +import posthog from 'posthog-js'; import { Secret } from 'src/app/models/secret'; +import { SecretsService } from 'src/app/services/secrets.service'; @Component({ - selector: 'app-edit-secret-dialog', - templateUrl: './edit-secret-dialog.component.html', - styleUrls: ['./edit-secret-dialog.component.css'], - providers: [provideNativeDateAdapter()], - imports: [ - CommonModule, - FormsModule, - ReactiveFormsModule, - MatDialogModule, - MatButtonModule, - MatInputModule, - MatFormFieldModule, - MatDatepickerModule, - MatIconModule, - MatCheckboxModule, - MatTooltipModule, - ] + selector: 'app-edit-secret-dialog', + templateUrl: './edit-secret-dialog.component.html', + styleUrls: ['./edit-secret-dialog.component.css'], + providers: [provideNativeDateAdapter()], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + MatDialogModule, + MatButtonModule, + MatInputModule, + MatFormFieldModule, + MatDatepickerModule, + MatIconModule, + MatCheckboxModule, + MatTooltipModule, + ], }) export class EditSecretDialogComponent { - public form: FormGroup; - public submitting = false; - public showValue = false; - public masterPassword = ''; - public masterPasswordError = ''; - public showMasterPassword = false; - public clearExpiration = false; - public minDate = new Date(); + public form: FormGroup; + public submitting = false; + public showValue = false; + public masterPassword = ''; + public masterPasswordError = ''; + public showMasterPassword = false; + public clearExpiration = false; + public minDate = new Date(); - constructor( - @Inject(MAT_DIALOG_DATA) public data: { secret: Secret }, - private fb: FormBuilder, - private dialogRef: MatDialogRef, - private _secrets: SecretsService, - private angulartics2: Angulartics2 - ) { - this.form = this.fb.group({ - value: ['', [Validators.required, Validators.maxLength(10000)]], - expiresAt: [data.secret.expiresAt ? new Date(data.secret.expiresAt) : null], - }); - } + constructor( + @Inject(MAT_DIALOG_DATA) public data: { secret: Secret }, + private fb: FormBuilder, + private dialogRef: MatDialogRef, + private _secrets: SecretsService, + private angulartics2: Angulartics2, + ) { + this.form = this.fb.group({ + value: ['', [Validators.required, Validators.maxLength(10000)]], + expiresAt: [data.secret.expiresAt ? new Date(data.secret.expiresAt) : null], + }); + } - toggleValueVisibility(): void { - this.showValue = !this.showValue; - } + toggleValueVisibility(): void { + this.showValue = !this.showValue; + } - toggleMasterPasswordVisibility(): void { - this.showMasterPassword = !this.showMasterPassword; - } + toggleMasterPasswordVisibility(): void { + this.showMasterPassword = !this.showMasterPassword; + } - get valueError(): string { - const control = this.form.get('value'); - if (control?.hasError('required')) return 'New value is required'; - if (control?.hasError('maxlength')) return 'Value must be 10000 characters or less'; - return ''; - } + get valueError(): string { + const control = this.form.get('value'); + if (control?.hasError('required')) return 'New value is required'; + if (control?.hasError('maxlength')) return 'Value must be 10000 characters or less'; + return ''; + } - onClearExpirationChange(checked: boolean): void { - this.clearExpiration = checked; - if (checked) { - this.form.get('expiresAt')?.disable(); - } else { - this.form.get('expiresAt')?.enable(); - } - } + onClearExpirationChange(checked: boolean): void { + this.clearExpiration = checked; + if (checked) { + this.form.get('expiresAt')?.disable(); + } else { + this.form.get('expiresAt')?.enable(); + } + } - onSubmit(): void { - if (this.form.invalid) return; + onSubmit(): void { + if (this.form.invalid) return; - if (this.data.secret.masterEncryption && !this.masterPassword) { - this.masterPasswordError = 'Master password is required'; - return; - } + if (this.data.secret.masterEncryption && !this.masterPassword) { + this.masterPasswordError = 'Master password is required'; + return; + } - this.submitting = true; - const formValue = this.form.getRawValue(); + this.submitting = true; + const formValue = this.form.getRawValue(); - const payload = { - value: formValue.value, - expiresAt: this.clearExpiration - ? null - : (formValue.expiresAt ? new Date(formValue.expiresAt).toISOString() : undefined), - }; + const payload = { + value: formValue.value, + expiresAt: this.clearExpiration + ? null + : formValue.expiresAt + ? new Date(formValue.expiresAt).toISOString() + : undefined, + }; - this._secrets.updateSecret( - this.data.secret.slug, - payload, - this.data.secret.masterEncryption ? this.masterPassword : undefined - ).subscribe({ - next: () => { - this.angulartics2.eventTrack.next({ - action: 'Secrets: secret updated successfully', - }); - this.submitting = false; - this.dialogRef.close(true); - }, - error: (err) => { - this.submitting = false; - if (err.status === 403) { - this.masterPasswordError = 'Invalid master password'; - } - } - }); - } + this._secrets + .updateSecret(this.data.secret.slug, payload, this.data.secret.masterEncryption ? this.masterPassword : undefined) + .subscribe({ + next: () => { + this.angulartics2.eventTrack.next({ + action: 'Secrets: secret updated successfully', + }); + posthog.capture('Secrets: secret updated successfully'); + this.submitting = false; + this.dialogRef.close(true); + }, + error: (err) => { + this.submitting = false; + if (err.status === 403) { + this.masterPasswordError = 'Invalid master password'; + } + }, + }); + } } diff --git a/frontend/src/app/components/secrets/secrets.component.ts b/frontend/src/app/components/secrets/secrets.component.ts index f1bad68e9..04e883180 100644 --- a/frontend/src/app/components/secrets/secrets.component.ts +++ b/frontend/src/app/components/secrets/secrets.component.ts @@ -1,181 +1,180 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatTableModule } from '@angular/material/table'; import { MatButtonModule } from '@angular/material/button'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatDialog } from '@angular/material/dialog'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; -import { MatMenuModule } from '@angular/material/menu'; import { MatInputModule } from '@angular/material/input'; -import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatMenuModule } from '@angular/material/menu'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { MatTableModule } from '@angular/material/table'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatChipsModule } from '@angular/material/chips'; -import { MatDialog } from '@angular/material/dialog'; -import { MatDividerModule } from '@angular/material/divider'; -import { Subscription, Subject } from 'rxjs'; -import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; -import { Angulartics2 } from 'angulartics2'; import { Title } from '@angular/platform-browser'; - -import { SecretsService } from 'src/app/services/secrets.service'; -import { CompanyService } from 'src/app/services/company.service'; +import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { Secret, SecretPagination } from 'src/app/models/secret'; -import { CreateSecretDialogComponent } from './create-secret-dialog/create-secret-dialog.component'; -import { EditSecretDialogComponent } from './edit-secret-dialog/edit-secret-dialog.component'; -import { DeleteSecretDialogComponent } from './delete-secret-dialog/delete-secret-dialog.component'; -import { AuditLogDialogComponent } from './audit-log-dialog/audit-log-dialog.component'; +import { CompanyService } from 'src/app/services/company.service'; +import { SecretsService } from 'src/app/services/secrets.service'; +import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; import { PlaceholderTableDataComponent } from '../skeletons/placeholder-table-data/placeholder-table-data.component'; import { AlertComponent } from '../ui-components/alert/alert.component'; -import { ProfileSidebarComponent } from '../profile/profile-sidebar/profile-sidebar.component'; +import { AuditLogDialogComponent } from './audit-log-dialog/audit-log-dialog.component'; +import { CreateSecretDialogComponent } from './create-secret-dialog/create-secret-dialog.component'; +import { DeleteSecretDialogComponent } from './delete-secret-dialog/delete-secret-dialog.component'; +import { EditSecretDialogComponent } from './edit-secret-dialog/edit-secret-dialog.component'; @Component({ - selector: 'app-secrets', - templateUrl: './secrets.component.html', - styleUrls: ['./secrets.component.css'], - imports: [ - CommonModule, - FormsModule, - MatTableModule, - MatButtonModule, - MatIconModule, - MatMenuModule, - MatInputModule, - MatFormFieldModule, - MatPaginatorModule, - MatTooltipModule, - MatChipsModule, - MatDividerModule, - PlaceholderTableDataComponent, - AlertComponent, - ProfileSidebarComponent, - ] + selector: 'app-secrets', + templateUrl: './secrets.component.html', + styleUrls: ['./secrets.component.css'], + imports: [ + CommonModule, + FormsModule, + MatTableModule, + MatButtonModule, + MatIconModule, + MatMenuModule, + MatInputModule, + MatFormFieldModule, + MatPaginatorModule, + MatTooltipModule, + MatChipsModule, + MatDividerModule, + PlaceholderTableDataComponent, + AlertComponent, + ProfileSidebarComponent, + ], }) export class SecretsComponent implements OnInit, OnDestroy { - public secrets: Secret[] = []; - public pagination: SecretPagination = { - total: 0, - currentPage: 1, - perPage: 20, - lastPage: 1, - }; - public loading = true; - public searchQuery = ''; - public displayedColumns = ['slug', 'masterEncryption', 'expiresAt', 'updatedAt', 'actions']; - public subscriptions: Subscription[] = []; - - private searchSubject = new Subject(); - - constructor( - private _secrets: SecretsService, - private _company: CompanyService, - private dialog: MatDialog, - private angulartics2: Angulartics2, - private title: Title - ) {} - - ngOnInit(): void { - this._company.getCurrentTabTitle().subscribe(tabTitle => { - this.title.setTitle(`Secrets | ${tabTitle || 'Rocketadmin'}`); - }); - - this.loadSecrets(); - - const searchSub = this.searchSubject.pipe( - debounceTime(300), - distinctUntilChanged() - ).subscribe(() => { - this.pagination.currentPage = 1; - this.loadSecrets(); - }); - this.subscriptions.push(searchSub); - - const updateSub = this._secrets.cast.subscribe(action => { - if (action) { - this.loadSecrets(); - } - }); - this.subscriptions.push(updateSub); - } - - ngOnDestroy(): void { - this.subscriptions.forEach(sub => sub.unsubscribe()); - } - - loadSecrets(): void { - this.loading = true; - this._secrets.fetchSecrets( - this.pagination.currentPage, - this.pagination.perPage, - this.searchQuery || undefined - ).subscribe(response => { - if (response) { - this.secrets = response.data; - this.pagination = response.pagination; - } - this.loading = false; - }); - } - - onSearchChange(query: string): void { - this.searchSubject.next(query); - } - - onPageChange(event: PageEvent): void { - this.pagination.currentPage = event.pageIndex + 1; - this.pagination.perPage = event.pageSize; - this.loadSecrets(); - } - - isExpired(secret: Secret): boolean { - if (!secret.expiresAt) return false; - return new Date(secret.expiresAt) < new Date(); - } - - isExpiringSoon(secret: Secret): boolean { - if (!secret.expiresAt) return false; - const expiresAt = new Date(secret.expiresAt); - const now = new Date(); - const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); - return expiresAt > now && expiresAt <= sevenDaysFromNow; - } - - openCreateDialog(): void { - this.dialog.open(CreateSecretDialogComponent, { - width: '500px', - }); - this.angulartics2.eventTrack.next({ - action: 'Secrets: create secret dialog opened', - }); - } - - openEditDialog(secret: Secret): void { - this.dialog.open(EditSecretDialogComponent, { - width: '500px', - data: { secret }, - }); - this.angulartics2.eventTrack.next({ - action: 'Secrets: edit secret dialog opened', - }); - } - - openDeleteDialog(secret: Secret): void { - this.dialog.open(DeleteSecretDialogComponent, { - width: '400px', - data: { secret }, - }); - this.angulartics2.eventTrack.next({ - action: 'Secrets: delete secret dialog opened', - }); - } - - openAuditLogDialog(secret: Secret): void { - this.dialog.open(AuditLogDialogComponent, { - width: '800px', - maxHeight: '80vh', - data: { secret }, - }); - this.angulartics2.eventTrack.next({ - action: 'Secrets: audit log dialog opened', - }); - } + public secrets: Secret[] = []; + public pagination: SecretPagination = { + total: 0, + currentPage: 1, + perPage: 20, + lastPage: 1, + }; + public loading = true; + public searchQuery = ''; + public displayedColumns = ['slug', 'masterEncryption', 'expiresAt', 'updatedAt', 'actions']; + public subscriptions: Subscription[] = []; + + private searchSubject = new Subject(); + + constructor( + private _secrets: SecretsService, + private _company: CompanyService, + private dialog: MatDialog, + private angulartics2: Angulartics2, + private title: Title, + ) {} + + ngOnInit(): void { + this._company.getCurrentTabTitle().subscribe((tabTitle) => { + this.title.setTitle(`Secrets | ${tabTitle || 'Rocketadmin'}`); + }); + + this.loadSecrets(); + + const searchSub = this.searchSubject.pipe(debounceTime(300), distinctUntilChanged()).subscribe(() => { + this.pagination.currentPage = 1; + this.loadSecrets(); + }); + this.subscriptions.push(searchSub); + + const updateSub = this._secrets.cast.subscribe((action) => { + if (action) { + this.loadSecrets(); + } + }); + this.subscriptions.push(updateSub); + } + + ngOnDestroy(): void { + this.subscriptions.forEach((sub) => sub.unsubscribe()); + } + + loadSecrets(): void { + this.loading = true; + this._secrets + .fetchSecrets(this.pagination.currentPage, this.pagination.perPage, this.searchQuery || undefined) + .subscribe((response) => { + if (response) { + this.secrets = response.data; + this.pagination = response.pagination; + } + this.loading = false; + }); + } + + onSearchChange(query: string): void { + this.searchSubject.next(query); + } + + onPageChange(event: PageEvent): void { + this.pagination.currentPage = event.pageIndex + 1; + this.pagination.perPage = event.pageSize; + this.loadSecrets(); + } + + isExpired(secret: Secret): boolean { + if (!secret.expiresAt) return false; + return new Date(secret.expiresAt) < new Date(); + } + + isExpiringSoon(secret: Secret): boolean { + if (!secret.expiresAt) return false; + const expiresAt = new Date(secret.expiresAt); + const now = new Date(); + const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); + return expiresAt > now && expiresAt <= sevenDaysFromNow; + } + + openCreateDialog(): void { + this.dialog.open(CreateSecretDialogComponent, { + width: '500px', + }); + this.angulartics2.eventTrack.next({ + action: 'Secrets: create secret dialog opened', + }); + posthog.capture('Secrets: create secret dialog opened'); + } + + openEditDialog(secret: Secret): void { + this.dialog.open(EditSecretDialogComponent, { + width: '500px', + data: { secret }, + }); + this.angulartics2.eventTrack.next({ + action: 'Secrets: edit secret dialog opened', + }); + posthog.capture('Secrets: edit secret dialog opened'); + } + + openDeleteDialog(secret: Secret): void { + this.dialog.open(DeleteSecretDialogComponent, { + width: '400px', + data: { secret }, + }); + this.angulartics2.eventTrack.next({ + action: 'Secrets: delete secret dialog opened', + }); + posthog.capture('Secrets: delete secret dialog opened'); + } + + openAuditLogDialog(secret: Secret): void { + this.dialog.open(AuditLogDialogComponent, { + width: '800px', + maxHeight: '80vh', + data: { secret }, + }); + this.angulartics2.eventTrack.next({ + action: 'Secrets: audit log dialog opened', + }); + posthog.capture('Secrets: audit log dialog opened'); + } } diff --git a/frontend/src/app/components/ui-components/user-password/user-password.component.html b/frontend/src/app/components/ui-components/user-password/user-password.component.html index 302c1b8e8..9a2550ee9 100644 --- a/frontend/src/app/components/ui-components/user-password/user-password.component.html +++ b/frontend/src/app/components/ui-components/user-password/user-password.component.html @@ -4,6 +4,7 @@ required minlength="8" passwordValidator angulartics2On="change" angularticsAction="Reg: password is changed" + (change)="posthog.capture('Reg: password is changed')" [(ngModel)]="value" (ngModelChange)="onFieldChange.emit($event)"> {{passwordHidden ? 'visibility_off' : 'visibility'}} diff --git a/frontend/src/app/components/ui-components/user-password/user-password.component.ts b/frontend/src/app/components/ui-components/user-password/user-password.component.ts index 97e5c3bc7..2cf6d7709 100644 --- a/frontend/src/app/components/ui-components/user-password/user-password.component.ts +++ b/frontend/src/app/components/ui-components/user-password/user-password.component.ts @@ -1,38 +1,37 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; - import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { PasswordStrengthMeterComponent } from 'angular-password-strength-meter'; -import { PasswordValidationDirective } from 'src/app/directives/passwordValidator.directive'; import { Angulartics2OnModule } from 'angulartics2'; +import posthog from 'posthog-js'; +import { PasswordValidationDirective } from 'src/app/directives/passwordValidator.directive'; @Component({ - selector: 'app-user-password', - templateUrl: './user-password.component.html', - styleUrls: ['./user-password.component.css'], - imports: [ - CommonModule, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatIconModule, - PasswordValidationDirective, - PasswordStrengthMeterComponent, - Angulartics2OnModule - ] + selector: 'app-user-password', + templateUrl: './user-password.component.html', + styleUrls: ['./user-password.component.css'], + imports: [ + CommonModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatIconModule, + PasswordValidationDirective, + PasswordStrengthMeterComponent, + Angulartics2OnModule, + ], }) export class UserPasswordComponent implements OnInit { + protected posthog = posthog; - @Input() value: string; - @Input() label: string; - @Output() onFieldChange = new EventEmitter(); - - public passwordHidden: boolean; + @Input() value: string; + @Input() label: string; + @Output() onFieldChange = new EventEmitter(); - ngOnInit(): void { - } + public passwordHidden: boolean; + ngOnInit(): void {} } diff --git a/frontend/src/app/components/user-settings/account-delete-confirmation/account-delete-confirmation.component.ts b/frontend/src/app/components/user-settings/account-delete-confirmation/account-delete-confirmation.component.ts index b23ae244d..f83a402fe 100644 --- a/frontend/src/app/components/user-settings/account-delete-confirmation/account-delete-confirmation.component.ts +++ b/frontend/src/app/components/user-settings/account-delete-confirmation/account-delete-confirmation.component.ts @@ -1,56 +1,54 @@ -import { Component, Inject, OnInit } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; -import { UserService } from 'src/app/services/user.service'; -import { Router } from '@angular/router'; -import { Angulartics2 } from 'angulartics2'; import { CommonModule } from '@angular/common'; +import { Component, Inject, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { Router } from '@angular/router'; +import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; +import { UserService } from 'src/app/services/user.service'; @Component({ - selector: 'app-account-delete-confirmation', - imports: [ - CommonModule, - MatDialogModule, - MatButtonModule, - MatProgressSpinnerModule - ], - templateUrl: './account-delete-confirmation.component.html', - styleUrls: ['./account-delete-confirmation.component.css'] + selector: 'app-account-delete-confirmation', + imports: [CommonModule, MatDialogModule, MatButtonModule, MatProgressSpinnerModule], + templateUrl: './account-delete-confirmation.component.html', + styleUrls: ['./account-delete-confirmation.component.css'], }) export class AccountDeleteConfirmationComponent implements OnInit { + public submitting: boolean = false; - public submitting: boolean = false; - - constructor( - @Inject(MAT_DIALOG_DATA) public data: any, - private _userService: UserService, - public dialogRef: MatDialogRef, - public router: Router, - private angulartics2: Angulartics2 - ) { } - - ngOnInit(): void { - } + constructor( + @Inject(MAT_DIALOG_DATA) public data: any, + private _userService: UserService, + public dialogRef: MatDialogRef, + public router: Router, + private angulartics2: Angulartics2, + ) {} - deleteAccount() { - this.submitting = true; - this._userService.deleteAccount(this.data) - .subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Delete account', - properties: { - email: this.data.email, - reason: this.data.reason, - message: this.data.message - } - }); - this.submitting = false; - this.dialogRef.close(); - }, - undefined, - () => this.submitting = false - ); - } + ngOnInit(): void {} + deleteAccount() { + this.submitting = true; + this._userService.deleteAccount(this.data).subscribe( + () => { + this.angulartics2.eventTrack.next({ + action: 'Delete account', + properties: { + email: this.data.email, + reason: this.data.reason, + message: this.data.message, + }, + }); + posthog.capture('Delete account', { + email: this.data.email, + reason: this.data.reason, + message: this.data.message, + }); + this.submitting = false; + this.dialogRef.close(); + }, + undefined, + () => (this.submitting = false), + ); + } } diff --git a/frontend/src/app/components/user-settings/enable-two-fa-dialog/enable-two-fa-dialog.component.ts b/frontend/src/app/components/user-settings/enable-two-fa-dialog/enable-two-fa-dialog.component.ts index d158dde23..abf9f0e01 100644 --- a/frontend/src/app/components/user-settings/enable-two-fa-dialog/enable-two-fa-dialog.component.ts +++ b/frontend/src/app/components/user-settings/enable-two-fa-dialog/enable-two-fa-dialog.component.ts @@ -1,54 +1,52 @@ import { Component, EventEmitter, Inject, Output } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { UserService } from 'src/app/services/user.service'; -import { GroupDeleteDialogComponent } from '../../users/group-delete-dialog/group-delete-dialog.component'; -import { Angulartics2 } from 'angulartics2'; import { FormsModule } from '@angular/forms'; -import { MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { MatButtonModule } from '@angular/material/button'; +import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; +import { UserService } from 'src/app/services/user.service'; +import { GroupDeleteDialogComponent } from '../../users/group-delete-dialog/group-delete-dialog.component'; @Component({ - selector: 'app-enable-two-fa-dialog', - imports: [ - FormsModule, - MatDialogModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule - ], - templateUrl: './enable-two-fa-dialog.component.html', - styleUrls: ['./enable-two-fa-dialog.component.css'] + selector: 'app-enable-two-fa-dialog', + imports: [FormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatButtonModule], + templateUrl: './enable-two-fa-dialog.component.html', + styleUrls: ['./enable-two-fa-dialog.component.css'], }) export class EnableTwoFADialogComponent { - @Output() confirm2FAerror = new EventEmitter(); + @Output() confirm2FAerror = new EventEmitter(); - public secondFAQRCode: string; - public submitting: boolean = false; + public secondFAQRCode: string; + public submitting: boolean = false; - constructor( - @Inject(MAT_DIALOG_DATA) public qr: any, - public dialogRef: MatDialogRef, - private _userService: UserService, - private angulartics2: Angulartics2, - ) {} + constructor( + @Inject(MAT_DIALOG_DATA) public qr: any, + public dialogRef: MatDialogRef, + private _userService: UserService, + private angulartics2: Angulartics2, + ) {} - verify2FA() { - this.submitting = true; + verify2FA() { + this.submitting = true; - this._userService.confirm2FA(this.secondFAQRCode) - .subscribe((res) => { - if (res.validated) { - this.secondFAQRCode = ''; - this.submitting = false; - this.dialogRef.close(); - this.angulartics2.eventTrack.next({ - action: 'User settings: 2fa enabled successfully', - }); - } - }, - (err) => { console.log('error !!!!', err); this.confirm2FAerror.emit(true); } - ) - } + this._userService.confirm2FA(this.secondFAQRCode).subscribe( + (res) => { + if (res.validated) { + this.secondFAQRCode = ''; + this.submitting = false; + this.dialogRef.close(); + this.angulartics2.eventTrack.next({ + action: 'User settings: 2fa enabled successfully', + }); + posthog.capture('User settings: 2fa enabled successfully'); + } + }, + (err) => { + console.log('error !!!!', err); + this.confirm2FAerror.emit(true); + }, + ); + } } diff --git a/frontend/src/app/components/user-settings/user-settings.component.html b/frontend/src/app/components/user-settings/user-settings.component.html index 5959b9cef..c09678e65 100644 --- a/frontend/src/app/components/user-settings/user-settings.component.html +++ b/frontend/src/app/components/user-settings/user-settings.component.html @@ -19,6 +19,7 @@

data-testid="user-name-input" angulartics2On="change" angularticsAction="User settings: user name is edited" + (change)="posthog.capture('User settings: user name is edited')" [(ngModel)]="userName"> @@ -23,7 +23,7 @@

User groups

angulartics2On="click" angularticsAction="Users access: edit group name is clicked" matTooltip="Edit group name" - (click)="openEditGroupNameDialog($event, groupItem.group)"> + (click)="openEditGroupNameDialog($event, groupItem.group); posthog.capture('Users access: edit group name is clicked')"> edit @@ -32,21 +32,21 @@

User groups

angulartics2On="click" angularticsAction="Users access: permissions is clicked" matTooltip="Configure permissions" - (click)="openPermissionsDialog(groupItem.group)"> + (click)="openPermissionsDialog(groupItem.group); posthog.capture('Users access: permissions is clicked')"> vpn_key @@ -65,7 +65,7 @@

User groups

angulartics2On="click" angularticsAction="Users access: delete user is clicked" matTooltip="Delete user" - (click)="openDeleteUserDialog(user, groupItem.group)"> + (click)="openDeleteUserDialog(user, groupItem.group); posthog.capture('Users access: delete user is clicked')"> person_remove diff --git a/frontend/src/app/components/users/users.component.ts b/frontend/src/app/components/users/users.component.ts index c210f0474..cec08bfff 100644 --- a/frontend/src/app/components/users/users.component.ts +++ b/frontend/src/app/components/users/users.component.ts @@ -9,6 +9,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { Title } from '@angular/platform-browser'; import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; import { differenceBy } from 'lodash-es'; +import posthog from 'posthog-js'; import { forkJoin, Observable, Subscription, take, tap } from 'rxjs'; import { Connection } from 'src/app/models/connection'; import { GroupUser, User, UserGroup, UserGroupInfo } from 'src/app/models/user'; @@ -46,6 +47,7 @@ import { UserDeleteDialogComponent } from './user-delete-dialog/user-delete-dial styleUrls: ['./users.component.css'], }) export class UsersComponent implements OnInit, OnDestroy { + protected posthog = posthog; public users: { [key: string]: GroupUser[] | 'empty' } = {}; public currentUser: User; public groups: UserGroupInfo[] | null = null; diff --git a/frontend/src/app/services/posthog.service.ts b/frontend/src/app/services/posthog.service.ts new file mode 100644 index 000000000..ea15ccece --- /dev/null +++ b/frontend/src/app/services/posthog.service.ts @@ -0,0 +1,26 @@ +import { DestroyRef, Injectable, NgZone } from '@angular/core'; +import { Router } from '@angular/router'; +import posthog from 'posthog-js'; +import { environment } from '../../environments/environment'; + +@Injectable({ providedIn: 'root' }) +export class PosthogService { + constructor( + private ngZone: NgZone, + private router: Router, + private destroyRef: DestroyRef, + ) { + if ((environment as any).saas && environment.production) { + this.initPostHog(); + } + } + + private initPostHog() { + this.ngZone.runOutsideAngular(() => { + posthog.init('phc_VPnWHIMj9UjhRLPr7shATjgL0J4KrWWOHkK3JwZbnkw', { + api_host: 'https://us.i.posthog.com', + defaults: '2025-11-30', + }); + }); + } +} diff --git a/frontend/src/app/services/tables.service.ts b/frontend/src/app/services/tables.service.ts index eb1fc93a0..72b2db20e 100644 --- a/frontend/src/app/services/tables.service.ts +++ b/frontend/src/app/services/tables.service.ts @@ -1,11 +1,12 @@ -import { BehaviorSubject, EMPTY, throwError } from 'rxjs'; -import { PersonalTableViewSettings, Rule, TableSettings, Widget } from '../models/table'; -import { HttpClient, } from '@angular/common/http'; +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { Angulartics2 } from 'angulartics2'; +import posthog from 'posthog-js'; +import { BehaviorSubject, EMPTY, throwError } from 'rxjs'; import { catchError, filter, map } from 'rxjs/operators'; import { AlertActionType, AlertType } from '../models/alert'; +import { PersonalTableViewSettings, Rule, TableSettings, Widget } from '../models/table'; import { NotificationsService } from './notifications.service'; export enum SortOrdering { @@ -207,6 +208,7 @@ export class TablesService { this.angulartics2.eventTrack.next({ action: 'Dashboard: db export failed', }); + posthog.capture('Dashboard: db export failed'); return EMPTY; }), ); @@ -234,6 +236,7 @@ export class TablesService { this.angulartics2.eventTrack.next({ action: 'Dashboard: db import failed', }); + posthog.capture('Dashboard: db import failed'); return EMPTY; }), ); @@ -549,6 +552,7 @@ export class TablesService { this.angulartics2.eventTrack.next({ action: 'AI: thread created', }); + posthog.capture('AI: thread created'); const responseMessage = res.body as string; return { threadId, responseMessage }; }), @@ -574,111 +578,117 @@ export class TablesService { }, }, ) + .pipe(map((res) => res.body as string)); + } + + updatePersonalTableViewSettings(connectionID: string, tableName: string, settings: PersonalTableViewSettings) { + return this._http + .put(`/settings/personal/${connectionID}`, settings, { + params: { + tableName, + }, + }) .pipe( - map((res) => res.body as string) - ) - ; - } - - updatePersonalTableViewSettings(connectionID: string, tableName: string, settings: PersonalTableViewSettings) { - return this._http.put(`/settings/personal/${connectionID}`, settings, { - params: { - tableName - } - }) - .pipe( - map(() => { - this.tables.next('settings'); - // this._notifications.showSuccessSnackbar('Table settings has been updated.') - }), - catchError((err) => { - console.log(err); - this._notifications.showAlert(AlertType.Error, {abstract: err.error.message, details: err.error.originalMessage}, [ - { - type: AlertActionType.Button, - caption: 'Dismiss', - action: (id: number) => this._notifications.dismissAlert() - } - ]); - return EMPTY; - }) - ); - } - - getSavedFilters(connectionID: string, tableName: string) { - return this._http.get(`/table-filters/${connectionID}/all`, { - params: { - tableName - } - }) - .pipe( - map((res) => { - return res - }), - catchError((err) => { - console.log(err); - return throwError(() => new Error(err.error.message)); - }) - ); - } - - createSavedFilter(connectionID: string, tableName: string, filters: object) { - return this._http.post(`/table-filters/${connectionID}`, filters, { - params: { - tableName - } - }) - .pipe( - map(res => { - this.tables.next('filters set saved'); - this._notifications.showSuccessSnackbar('Saved filters have been updated.') - return res - }), - catchError((err) => { - console.log(err); - this._notifications.showErrorSnackbar(err.error.message); - return EMPTY; - }) - ) - } - - updateSavedFilter(connectionID: string, tableName: string, filtersId: string, filters: object) { - return this._http.put(`/table-filters/${connectionID}/${filtersId}`, filters, { - params: { - tableName - } - }) - .pipe( - map(res => { - this.tables.next('filters set updated'); - this._notifications.showSuccessSnackbar('Saved filter has been updated.') - return res - }), - catchError((err) => { - console.log(err); - this._notifications.showErrorSnackbar(err.error.message); - return EMPTY; - }) - ); - } - - deleteSavedFilter(connectionID: string, tableName: string, filterId: string) { - return this._http.delete(`/table-filters/${connectionID}/${filterId}`, { - params: { - tableName - } - }) - .pipe( - map(res => { - this.tables.next('delete saved filters'); - this._notifications.showSuccessSnackbar('Saved filter has been deleted.') - return res - }), - catchError((err) => { - console.log(err); - this._notifications.showErrorSnackbar(err.error.message); - return EMPTY; - }) - ) - } + map(() => { + this.tables.next('settings'); + // this._notifications.showSuccessSnackbar('Table settings has been updated.') + }), + catchError((err) => { + console.log(err); + this._notifications.showAlert( + AlertType.Error, + { abstract: err.error.message, details: err.error.originalMessage }, + [ + { + type: AlertActionType.Button, + caption: 'Dismiss', + action: (id: number) => this._notifications.dismissAlert(), + }, + ], + ); + return EMPTY; + }), + ); + } + + getSavedFilters(connectionID: string, tableName: string) { + return this._http + .get(`/table-filters/${connectionID}/all`, { + params: { + tableName, + }, + }) + .pipe( + map((res) => { + return res; + }), + catchError((err) => { + console.log(err); + return throwError(() => new Error(err.error.message)); + }), + ); + } + + createSavedFilter(connectionID: string, tableName: string, filters: object) { + return this._http + .post(`/table-filters/${connectionID}`, filters, { + params: { + tableName, + }, + }) + .pipe( + map((res) => { + this.tables.next('filters set saved'); + this._notifications.showSuccessSnackbar('Saved filters have been updated.'); + return res; + }), + catchError((err) => { + console.log(err); + this._notifications.showErrorSnackbar(err.error.message); + return EMPTY; + }), + ); + } + + updateSavedFilter(connectionID: string, tableName: string, filtersId: string, filters: object) { + return this._http + .put(`/table-filters/${connectionID}/${filtersId}`, filters, { + params: { + tableName, + }, + }) + .pipe( + map((res) => { + this.tables.next('filters set updated'); + this._notifications.showSuccessSnackbar('Saved filter has been updated.'); + return res; + }), + catchError((err) => { + console.log(err); + this._notifications.showErrorSnackbar(err.error.message); + return EMPTY; + }), + ); + } + + deleteSavedFilter(connectionID: string, tableName: string, filterId: string) { + return this._http + .delete(`/table-filters/${connectionID}/${filterId}`, { + params: { + tableName, + }, + }) + .pipe( + map((res) => { + this.tables.next('delete saved filters'); + this._notifications.showSuccessSnackbar('Saved filter has been deleted.'); + return res; + }), + catchError((err) => { + console.log(err); + this._notifications.showErrorSnackbar(err.error.message); + return EMPTY; + }), + ); + } } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 6767a3cd5..771da16b0 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -17,7 +17,6 @@ import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; import { CookieService } from 'ngx-cookie-service'; import { MarkdownModule, provideMarkdown } from 'ngx-markdown'; import { NgxStripeModule } from 'ngx-stripe'; -import posthog from 'posthog-js'; import { AppComponent } from './app/app.component'; import { AppRoutingModule } from './app/app-routing.module'; import { ConfigModule } from './app/modules/config.module'; @@ -85,13 +84,6 @@ if ((environment as any).saas) { }); } -if ((environment as any).saas) { - posthog.init('phc_VPnWHIMj9UjhRLPr7shATjgL0J4KrWWOHkK3JwZbnkw', { - api_host: 'https://us.i.posthog.com', - defaults: '2025-11-30', - }); -} - bootstrapApplication(AppComponent, { providers: [ importProvidersFrom(