From bd9ea272c777ebab8a5adac5a6e7e86bab9382fa Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Wed, 25 Feb 2026 11:22:49 +0000 Subject: [PATCH 1/2] Add AI auto-configure route for new connections Redirect users to /auto-configure/:connection-id after adding a connection, where the frontend calls GET /api/ai/v2/setup/:connection-id to trigger AI-based dashboard configuration. Remove the backend fire-and-forget AI scan from connection creation so configuration is driven by the frontend route. Co-Authored-By: Claude Opus 4.6 --- .../use-cases/create-connection.use.case.ts | 87 +++++++------------ frontend/src/app/app-routing.module.ts | 7 ++ .../auto-configure.component.css | 30 +++++++ .../auto-configure.component.html | 12 +++ .../auto-configure.component.ts | 44 ++++++++++ .../connect-db/connect-db.component.ts | 2 +- 6 files changed, 125 insertions(+), 57 deletions(-) create mode 100644 frontend/src/app/components/auto-configure/auto-configure.component.css create mode 100644 frontend/src/app/components/auto-configure/auto-configure.component.html create mode 100644 frontend/src/app/components/auto-configure/auto-configure.component.ts diff --git a/backend/src/entities/connection/use-cases/create-connection.use.case.ts b/backend/src/entities/connection/use-cases/create-connection.use.case.ts index 1439aac60..9e14f404a 100644 --- a/backend/src/entities/connection/use-cases/create-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/create-connection.use.case.ts @@ -1,13 +1,10 @@ import { BadRequestException, Inject, Injectable, InternalServerErrorException, Scope } from '@nestjs/common'; import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js'; -import * as Sentry from '@sentry/node'; import AbstractUseCase from '../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../common/data-injection.tokens.js'; import { Messages } from '../../../exceptions/text/messages.js'; -import { Encryptor } from '../../../helpers/encryption/encryptor.js'; import { isConnectionTypeAgent, slackPostMessage } from '../../../helpers/index.js'; -import { SharedJobsService } from '../../shared-jobs/shared-jobs.service.js'; import { UserRoleEnum } from '../../user/enums/user-role.enum.js'; import { UserEntity } from '../../user/user.entity.js'; import { CreateConnectionDs } from '../application/data-structures/create-connection.ds.js'; @@ -27,7 +24,6 @@ export class CreateConnectionUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, - private readonly sharedJobsService: SharedJobsService, ) { super(); } @@ -51,79 +47,58 @@ export class CreateConnectionUseCase await validateCreateConnectionData(createConnectionData); createConnectionData = await processAWSConnection(createConnectionData); - let isConnectionTestedSuccessfully: boolean = false; if (!isConnectionTypeAgent(createConnectionData.connection_parameters.type)) { const connectionParamsCopy = { ...createConnectionData.connection_parameters, }; const dao = getDataAccessObject(connectionParamsCopy); try { - const testResult = await dao.testConnect(); - isConnectionTestedSuccessfully = testResult.result; + await dao.testConnect(); } catch (e) { const text: string = e.message.toLowerCase(); - isConnectionTestedSuccessfully = false; if (text.includes('ssl required') || text.includes('ssl connection required')) { createConnectionData.connection_parameters.ssl = true; connectionParamsCopy.ssl = true; try { const updatedDao = getDataAccessObject(connectionParamsCopy); - const sslTestResult = await updatedDao.testConnect(); - isConnectionTestedSuccessfully = sslTestResult.result; + await updatedDao.testConnect(); } catch (_e) { - isConnectionTestedSuccessfully = false; createConnectionData.connection_parameters.ssl = false; connectionParamsCopy.ssl = false; } } } } - let connectionCopy: ConnectionEntity = null; - try { - const createdConnection: ConnectionEntity = await buildConnectionEntity(createConnectionData, connectionAuthor); - const savedConnection: ConnectionEntity = - await this._dbContext.connectionRepository.saveNewConnection(createdConnection); + const createdConnection: ConnectionEntity = await buildConnectionEntity(createConnectionData, connectionAuthor); + const savedConnection: ConnectionEntity = + await this._dbContext.connectionRepository.saveNewConnection(createdConnection); - connectionCopy = { ...savedConnection } as ConnectionEntity; - if (savedConnection.masterEncryption && masterPwd && !isConnectionTypeAgent(savedConnection.type)) { - connectionCopy = Encryptor.decryptConnectionCredentials(connectionCopy, masterPwd); - } - - let token: string; - if (isConnectionTypeAgent(savedConnection.type)) { - token = await this._dbContext.agentRepository.createNewAgentForConnectionAndReturnToken(savedConnection); - } - const createdAdminGroup = await this._dbContext.groupRepository.createdAdminGroupInConnection( - savedConnection, - connectionAuthor, - ); - await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup); - delete createdAdminGroup.connection; - await this._dbContext.userRepository.saveUserEntity(connectionAuthor); - createdConnection.groups = [createdAdminGroup]; - const foundUserCompany = await this._dbContext.companyInfoRepository.findOneCompanyInfoByUserIdWithConnections( - connectionAuthor.id, - ); - if (foundUserCompany) { - const connection = await this._dbContext.connectionRepository.findOne({ - where: { id: savedConnection.id }, - }); - connection.company = foundUserCompany; - await this._dbContext.connectionRepository.saveUpdatedConnection(connection); - } - await slackPostMessage( - Messages.USER_CREATED_CONNECTION(connectionAuthor.email, createConnectionData.connection_parameters.type), - ); - const connectionRO = buildCreatedConnectionDs(savedConnection, token, masterPwd); - return connectionRO; - } finally { - if (isConnectionTestedSuccessfully && !isConnectionTypeAgent(connectionCopy.type)) { - // Fire-and-forget: run AI scan in background without blocking response - this.sharedJobsService.scanDatabaseAndCreateSettingsAndWidgetsWithAI(connectionCopy).catch((error) => { - console.error('Background AI scan failed:', error); - Sentry.captureException(error); - }); - } + let token: string; + if (isConnectionTypeAgent(savedConnection.type)) { + token = await this._dbContext.agentRepository.createNewAgentForConnectionAndReturnToken(savedConnection); } + const createdAdminGroup = await this._dbContext.groupRepository.createdAdminGroupInConnection( + savedConnection, + connectionAuthor, + ); + await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup); + delete createdAdminGroup.connection; + await this._dbContext.userRepository.saveUserEntity(connectionAuthor); + createdConnection.groups = [createdAdminGroup]; + const foundUserCompany = await this._dbContext.companyInfoRepository.findOneCompanyInfoByUserIdWithConnections( + connectionAuthor.id, + ); + if (foundUserCompany) { + const connection = await this._dbContext.connectionRepository.findOne({ + where: { id: savedConnection.id }, + }); + connection.company = foundUserCompany; + await this._dbContext.connectionRepository.saveUpdatedConnection(connection); + } + await slackPostMessage( + Messages.USER_CREATED_CONNECTION(connectionAuthor.email, createConnectionData.connection_parameters.type), + ); + const connectionRO = buildCreatedConnectionDs(savedConnection, token, masterPwd); + return connectionRO; } } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index e21c149c0..12966d378 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -137,6 +137,13 @@ const routes: Routes = [ canActivate: [AuthGuard], title: 'Upgraded successfully | Rocketadmin', }, + { + path: 'auto-configure/:connection-id', + loadComponent: () => + import('./components/auto-configure/auto-configure.component').then((m) => m.AutoConfigureComponent), + canActivate: [AuthGuard], + title: 'Auto-configure | Rocketadmin', + }, { path: 'edit-db/:connection-id', loadComponent: () => import('./components/connect-db/connect-db.component').then((m) => m.ConnectDBComponent), diff --git a/frontend/src/app/components/auto-configure/auto-configure.component.css b/frontend/src/app/components/auto-configure/auto-configure.component.css new file mode 100644 index 000000000..c4d15122c --- /dev/null +++ b/frontend/src/app/components/auto-configure/auto-configure.component.css @@ -0,0 +1,30 @@ +.auto-configure { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; + height: 100vh; +} + +.auto-configure__title { + color: var(--mat-sidenav-content-text-color); + margin: 0; + font-weight: 500; +} + +.auto-configure__text { + color: var(--mat-sidenav-content-text-color); + opacity: 0.7; + margin: 0; + max-width: 400px; + text-align: center; +} + +.auto-configure__notice { + color: var(--mat-sidenav-content-text-color); + opacity: 0.7; + margin: 0; + text-align: center; + max-width: 400px; +} diff --git a/frontend/src/app/components/auto-configure/auto-configure.component.html b/frontend/src/app/components/auto-configure/auto-configure.component.html new file mode 100644 index 000000000..5b4ea8816 --- /dev/null +++ b/frontend/src/app/components/auto-configure/auto-configure.component.html @@ -0,0 +1,12 @@ +@if (loading()) { +
+ +

Auto-configuring your dashboard

+

Please wait while we analyze your database and set up the best configuration. This may take a moment.

+
+} @else if (errorMessage()) { +
+

{{ errorMessage() }}

+ Continue to dashboard +
+} diff --git a/frontend/src/app/components/auto-configure/auto-configure.component.ts b/frontend/src/app/components/auto-configure/auto-configure.component.ts new file mode 100644 index 000000000..322ee6548 --- /dev/null +++ b/frontend/src/app/components/auto-configure/auto-configure.component.ts @@ -0,0 +1,44 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; + +import { ApiService } from '../../services/api.service'; + +@Component({ + selector: 'app-auto-configure', + templateUrl: './auto-configure.component.html', + styleUrls: ['./auto-configure.component.css'], + imports: [MatProgressSpinnerModule, MatButtonModule, RouterModule], +}) +export class AutoConfigureComponent implements OnInit { + private _route = inject(ActivatedRoute); + private _router = inject(Router); + private _api = inject(ApiService); + + protected connectionId = signal(null); + protected loading = signal(true); + protected errorMessage = signal(null); + + ngOnInit(): void { + const connectionId = this._route.snapshot.paramMap.get('connection-id'); + if (!connectionId) { + this._router.navigate(['/connections-list']); + return; + } + + this.connectionId.set(connectionId); + this._configure(connectionId); + } + + private async _configure(connectionId: string): Promise { + const result = await this._api.get(`/ai/v2/setup/${connectionId}`, { responseType: 'text' }); + + if (result !== null) { + this._router.navigate([`/dashboard/${connectionId}`]); + } else { + this.loading.set(false); + this.errorMessage.set('Auto-configuration could not be completed. You can still configure your tables manually.'); + } + } +} 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 c73809296..56182657f 100644 --- a/frontend/src/app/components/connect-db/connect-db.component.ts +++ b/frontend/src/app/components/connect-db/connect-db.component.ts @@ -220,7 +220,7 @@ export class ConnectDBComponent implements OnInit { this.connectionToken = res.token; this.connectionID = res.id; } else { - this.router.navigate([`/dashboard/${createdConnectionID}`]); + this.router.navigate([`/auto-configure/${createdConnectionID}`]); } this.angulartics2.eventTrack.next({ action: 'Connect DB: connection is added successfully', From 8d9fba241908e4a54fdf33e7c6e7e7e0afff062e Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Wed, 25 Feb 2026 12:22:33 +0000 Subject: [PATCH 2/2] Fix test: add auto-configure route to connect-db spec router Co-Authored-By: Claude Opus 4.6 --- .../app/components/connect-db/connect-db.component.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/connect-db/connect-db.component.spec.ts b/frontend/src/app/components/connect-db/connect-db.component.spec.ts index 6bd6680c1..d140129e6 100644 --- a/frontend/src/app/components/connect-db/connect-db.component.spec.ts +++ b/frontend/src/app/components/connect-db/connect-db.component.spec.ts @@ -78,7 +78,10 @@ describe('ConnectDBComponent', () => { ], providers: [ provideHttpClient(), - provideRouter([{ path: 'dashboard/:id', component: ConnectDBComponent }]), + provideRouter([ + { path: 'dashboard/:id', component: ConnectDBComponent }, + { path: 'auto-configure/:connection-id', component: ConnectDBComponent }, + ]), { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ConnectDBComponent),