From 3efc7bfc274d94a9170c00c7988ba1fe1e99a3a0 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Wed, 11 Feb 2026 12:46:09 +0000 Subject: [PATCH] add PostHog analytics tracking alongside Amplitude Create PosthogService following official Angular guide with ngZone.runOutsideAngular initialization. Add posthog.capture() calls alongside all existing Angulartics2 eventTrack.next() calls and template directives. PostHog only initializes in production SaaS environment. Co-Authored-By: Claude Opus 4.6 --- frontend/src/app/app.component.html | 2 +- frontend/src/app/app.component.ts | 5 + .../api-keys/api-keys.component.html | 1 + .../components/api-keys/api-keys.component.ts | 164 +-- .../app/components/audit/audit.component.html | 4 +- .../app/components/audit/audit.component.ts | 4 +- .../branding/branding.component.html | 2 + .../components/branding/branding.component.ts | 526 ++++---- .../chart-delete-dialog.component.ts | 2 + .../charts/chart-edit/chart-edit.component.ts | 4 + .../charts-list/charts-list.component.ts | 8 +- .../company-member-invitation.component.ts | 123 +- .../components/company/company.component.html | 18 +- .../components/company/company.component.ts | 842 ++++++------ .../delete-domain-dialog.component.ts | 57 +- .../delete-member-dialog.component.ts | 66 +- .../invite-member-dialog.component.html | 3 +- .../invite-member-dialog.component.ts | 3 + .../revoke-invitation-dialog.component.ts | 60 +- .../connect-db/connect-db.component.html | 18 +- .../connect-db/connect-db.component.ts | 844 ++++++------ .../db-connection-delete-dialog.component.ts | 114 +- .../cassandra-credentials-form.component.html | 13 + .../cassandra-credentials-form.component.ts | 47 +- ...clickhouse-credentials-form.component.html | 12 + .../clickhouse-credentials-form.component.ts | 46 +- .../db2-credentials-form.component.html | 13 + .../db2-credentials-form.component.ts | 41 +- .../dynamodb-credentials-form.component.html | 3 + .../dynamodb-credentials-form.component.ts | 42 +- .../elastic-credentials-form.component.html | 13 + .../elastic-credentials-form.component.ts | 44 +- .../mongodb-credentials-form.component.html | 13 + .../mongodb-credentials-form.component.ts | 44 +- .../mssql-credentials-form.component.html | 14 + .../mssql-credentials-form.component.ts | 41 +- .../mysql-credentials-form.component.html | 12 + .../mysql-credentials-form.component.ts | 43 +- .../oracledb-credentials-form.component.html | 14 + .../oracledb-credentials-form.component.ts | 44 +- .../postgres-credentials-form.component.html | 13 + .../postgres-credentials-form.component.ts | 44 +- .../redis-credentials-form.component.html | 12 + .../redis-credentials-form.component.ts | 41 +- .../master-encryption-password.component.html | 1 + .../master-encryption-password.component.ts | 89 +- .../connection-settings.component.html | 7 + .../connection-settings.component.ts | 5 + .../demo-connections.component.html | 1 + .../demo-connections.component.ts | 106 +- .../own-connections.component.html | 9 +- .../own-connections.component.ts | 2 + .../dashboard/dashboard.component.html | 6 +- .../dashboard/dashboard.component.ts | 18 +- .../db-table-actions.component.html | 4 +- .../db-table-actions.component.ts | 787 +++++------ .../db-table-ai-panel.component.ts | 971 +++++++------- .../db-table-export-dialog.component.ts | 110 +- .../db-table-filters-dialog.component.html | 6 +- .../db-table-filters-dialog.component.ts | 2 + .../db-table-import-dialog.component.ts | 78 +- .../db-table-settings.component.ts | 434 +++--- .../db-table-view.component.html | 36 +- .../db-table-view/db-table-view.component.ts | 2 + .../db-table-widgets.component.html | 4 +- .../db-table-widgets.component.ts | 3 + .../saved-filters-dialog.component.ts | 876 ++++++------ .../saved-filters-panel.component.html | 4 +- .../saved-filters-panel.component.ts | 1177 +++++++++-------- .../dashboard-delete-dialog.component.ts | 2 + .../dashboard-edit-dialog.component.ts | 3 + .../dashboard-view.component.ts | 5 + .../dashboards-list.component.ts | 5 + .../widget-delete-dialog.component.ts | 2 + .../widget-edit-dialog.component.ts | 5 +- .../app/components/login/login.component.html | 10 +- .../app/components/login/login.component.ts | 11 + .../password-change.component.ts | 91 +- .../registration/registration.component.html | 7 +- .../registration/registration.component.ts | 7 + .../create-secret-dialog.component.ts | 209 ++- .../delete-secret-dialog.component.ts | 66 +- .../edit-secret-dialog.component.ts | 201 +-- .../components/secrets/secrets.component.ts | 329 +++-- .../user-password.component.html | 1 + .../user-password/user-password.component.ts | 45 +- .../account-delete-confirmation.component.ts | 88 +- .../enable-two-fa-dialog.component.ts | 80 +- .../user-settings.component.html | 3 + .../user-settings/user-settings.component.ts | 361 ++--- .../group-add-dialog.component.ts | 84 +- .../group-delete-dialog.component.ts | 73 +- .../permissions-add-dialog.component.ts | 303 ++--- .../user-add-dialog.component.ts | 99 +- .../app/components/users/users.component.html | 12 +- .../app/components/users/users.component.ts | 2 + frontend/src/app/services/posthog.service.ts | 26 + frontend/src/app/services/tables.service.ts | 228 ++-- frontend/src/main.ts | 8 - 99 files changed, 5551 insertions(+), 5032 deletions(-) create mode 100644 frontend/src/app/services/posthog.service.ts 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 0b01a4d7d..91c0cccb0 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 caa969c21..40f240e3b 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, 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', @@ -122,12 +123,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'); } openDeleteDialog(query: SavedQuery): void { @@ -138,6 +141,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 40cfc1143..ed2e9cd32 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(