Add hosted database creation frontend#1674
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a “Hosted PostgreSQL” provisioning flow to the Connections UI for SaaS admins, including API integration, UI entry points, and a credentials/success dialog.
Changes:
- Introduces
HostedDatabaseService+ models to provision a hosted DB and create a corresponding RocketAdmin connection. - Extends
OwnConnectionsComponentto expose hosted-DB CTAs (empty-state + FAB) and orchestrate provisioning, connection creation, notifications, and dialog display. - Adds
HostedDatabaseSuccessDialogComponentto present credentials, copy-to-clipboard, and navigation actions.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/app/services/hosted-database.service.ts | New service for hosted DB provisioning and connection creation with error mapping. |
| frontend/src/app/services/hosted-database.service.spec.ts | New unit tests for the hosted DB service (happy paths). |
| frontend/src/app/models/hosted-database.ts | New types for hosted DB creation + connection payload/response. |
| frontend/src/app/components/connections-list/own-connections/own-connections.component.ts | Adds SaaS/admin gating and provisioning flow orchestration. |
| frontend/src/app/components/connections-list/own-connections/own-connections.component.html | Adds hosted DB CTAs and adjusts connection logo alt text. |
| frontend/src/app/components/connections-list/own-connections/own-connections.component.css | Styles hosted DB CTA and adjusts FAB layout for mobile. |
| frontend/src/app/components/connections-list/own-connections/own-connections.component.spec.ts | Adds tests for hosted DB CTA visibility and provisioning orchestration. |
| frontend/src/app/components/connections-list/hosted-database-success-dialog/hosted-database-success-dialog.component.ts | New dialog component for showing/copying credentials and tracking events. |
| frontend/src/app/components/connections-list/hosted-database-success-dialog/hosted-database-success-dialog.component.html | Dialog UI for success/partial-success states and actions. |
| frontend/src/app/components/connections-list/hosted-database-success-dialog/hosted-database-success-dialog.component.css | Dialog styling + responsive adjustments. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private _getErrorMessage(error: unknown): string { | ||
| if (error && typeof error === 'object' && 'error' in error) { | ||
| const responseError = (error as { error?: { message?: string } }).error; | ||
| if (responseError?.message) { | ||
| return responseError.message; | ||
| } | ||
| } | ||
|
|
||
| if (error instanceof Error && error.message) { | ||
| return error.message; | ||
| } | ||
|
|
||
| return 'Unknown error'; |
| <img *ngIf="connectionItem.connection.connection_properties?.logo_url; else noLogo" | ||
| [src]="connectionItem.connection.connection_properties?.logo_url" | ||
| class="connectionLogoPreview__logo" | ||
| [alt]="connectionItem.connection.connection_properties?.company_name"> | ||
| alt="Rocketadmin connection"> | ||
| <ng-template #noLogo> |
| .addConnectionLink { | ||
| appearance: none; | ||
| background: transparent; | ||
| border: none; | ||
| cursor: pointer; | ||
| display: flex; |
| expect(request.request.method).toBe('POST'); | ||
| expect(request.request.body).toEqual(payload); | ||
| request.flush({ id: 'connection-id' }); | ||
| }); |
…dialog for free users - Remove redundant /saas/connection/hosted request; use only /saas/hosted-database/create - Switch hosted database service from HttpClient to ApiService - Convert creatingHostedDatabase to Angular signal - Add plan selection dialog (free tiny node vs scalable pay-as-you-go) shown for free/null subscription users Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show hosted PostgreSQL tiers (Tiny node for free plan, Scalable for paid plans) in a new comparison table between Databases and Users sections. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…x flaky test Copy credentials now uses a standard PostgreSQL connection string format instead of multi-line key-value pairs. Also fix flaky test by setting subscriptionLevel on mock user to avoid triggering the plan dialog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds hosted PostgreSQL provisioning: new plan-selection and success dialogs, a HostedDatabaseService and model, UI integration in connections and upgrade pages, tests, and related styles and templates enabling SaaS-admin gated creation and credential display. Changes
Sequence DiagramsequenceDiagram
actor User
participant OwnConnections as OwnConnections<br/>Component
participant PlanDialog as PlanDialog<br/>Component
participant HostedService as HostedDatabase<br/>Service
participant Backend as Backend API
participant SuccessDialog as SuccessDialog<br/>Component
User->>OwnConnections: Click "Create Hosted Database"
OwnConnections->>PlanDialog: Open plan selection dialog
User->>PlanDialog: Select plan (free or upgrade)
alt User selects upgrade
PlanDialog-->>OwnConnections: Return 'upgrade'
OwnConnections->>User: Redirect to /upgrade
else User selects free
PlanDialog-->>OwnConnections: Return 'free'
OwnConnections->>HostedService: createHostedDatabase(companyId)
HostedService->>Backend: POST /saas/hosted-database/create/{companyId}
Backend-->>HostedService: CreatedHostedDatabase
HostedService-->>OwnConnections: Return created DB
OwnConnections->>OwnConnections: Refresh connections list
OwnConnections->>SuccessDialog: Open with credentials & status
User->>SuccessDialog: View / Copy / Navigate
SuccessDialog-->>User: Confirmations / Actions
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
frontend/src/app/components/connections-list/own-connections/own-connections.component.spec.ts (2)
117-140: Consider adding test coverage for error handling and free-plan dialog flow.The current tests cover the happy path well, but the component has additional branches worth testing:
- Error handling when
createHostedDatabaserejects- Free-plan user flow where
_openPlanDialog()is invokedThese would improve confidence in the error alert display and plan selection UX.
137-139: Consider asserting on dialog arguments for more thorough verification.The test verifies that
dialog.openwas called, but doesn't verify the dialog component or data passed. Adding argument assertions would catch regressions in the dialog configuration.💡 Suggested improvement
expect(hostedDatabaseService.createHostedDatabase).toHaveBeenCalledWith('company-id'); expect(connectionsService.fetchConnections).toHaveBeenCalled(); - expect(dialog.open).toHaveBeenCalled(); + expect(dialog.open).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + data: expect.objectContaining({ + hostedDatabase: expect.objectContaining({ id: 'hosted-db-id' }), + connectionId: null, + }), + }), + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/app/components/connections-list/own-connections/own-connections.component.spec.ts` around lines 137 - 139, The test currently only checks that dialog.open was called; update it to assert the actual dialog component and configuration passed to dialog.open: verify that dialog.open was invoked with the CreateHostedDatabaseDialogComponent (or the specific dialog class used when creating a hosted DB) and an options object whose data contains the company id ('company-id') (and any relevant flags like disableClose or width if used). Replace the generic expect(dialog.open).toHaveBeenCalled() with an assertion that inspects the first and second arguments passed to dialog.open to ensure the component and data payload are correct.frontend/src/app/services/hosted-database.service.ts (1)
11-13: Clarify whennullis returned vs. when an error is thrown.The return type
Promise<CreatedHostedDatabase | null>suggestsnullcan be returned, but the implementation directly returns theApiService.post()result. If the API service throws on errors (which the consumer's try/catch suggests), consider whether the| nullis needed, or document whennullwould be returned.💡 Option A: If null is never returned, simplify the type
- createHostedDatabase(companyId: string): Promise<CreatedHostedDatabase | null> { + createHostedDatabase(companyId: string): Promise<CreatedHostedDatabase> { return this._api.post<CreatedHostedDatabase>(`/saas/hosted-database/create/${companyId}`, {}); }💡 Option B: If null should be returned on certain conditions, add explicit handling
- createHostedDatabase(companyId: string): Promise<CreatedHostedDatabase | null> { - return this._api.post<CreatedHostedDatabase>(`/saas/hosted-database/create/${companyId}`, {}); + async createHostedDatabase(companyId: string): Promise<CreatedHostedDatabase | null> { + try { + return await this._api.post<CreatedHostedDatabase>(`/saas/hosted-database/create/${companyId}`, {}); + } catch { + return null; + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/app/services/hosted-database.service.ts` around lines 11 - 13, The return type Promise<CreatedHostedDatabase | null> is misleading because createHostedDatabase simply returns this._api.post(...) which throws on errors; remove the nullable union and change the signature to Promise<CreatedHostedDatabase> (or, alternatively, if you intend to return null for specific API responses, catch errors from this._api.post in createHostedDatabase, inspect the response/status and explicitly return null for those cases and rethrow for others); update callers accordingly. Ensure you modify the createHostedDatabase method signature and either remove the | null or add the explicit try/catch handling inside createHostedDatabase to return null for the intended conditions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@frontend/src/app/components/connections-list/hosted-database-success-dialog/hosted-database-success-dialog.component.ts`:
- Around line 28-31: The connection string built in the credentialsText getter
currently interpolates raw username and password and can break if they contain
reserved characters; update the credentialsText getter (credentialsText) to
URL-encode the username and password (from this.data.hostedDatabase.username and
.password) before interpolating them into the URI (use a standard encoder like
encodeURIComponent or the project's URL-encoding utility) while leaving
hostname, port and databaseName unchanged so the returned string is a valid
postgres URI.
In
`@frontend/src/app/components/connections-list/own-connections/own-connections.component.ts`:
- Around line 138-140: The early silent return when hostedDatabase is falsy
hides an unexpected error from users; update the block that currently reads "if
(!hostedDatabase) { return; }" to instead log the unexpected falsy value (e.g.
console.error or your logger) and show a user-facing error notification/dialog
using the component's existing UI notification service (e.g.
this.notificationService.error(...), this.toastService.showError(...) or
this.dialogService.open(...)); only proceed to the success flow (show success
dialog/continue logic) when hostedDatabase is truthy so users get clear feedback
in both success and failure cases.
- Around line 116-120: Remove the debug console.log statements in
OwnConnectionsComponent around the free-plan check: delete the console.log that
logs '[HostedDB] subscriptionLevel:' and the one that logs '[HostedDB] plan
dialog choice:' (the lines referencing subscriptionLevel, isFreePlan and the
result of this._openPlanDialog()). Leave the existing logic (the isFreePlan
conditional and the await this._openPlanDialog() call) intact; if structured
logging is required, replace console.log with the app's logger (e.g.,
this.logger.debug) instead of console methods.
---
Nitpick comments:
In
`@frontend/src/app/components/connections-list/own-connections/own-connections.component.spec.ts`:
- Around line 137-139: The test currently only checks that dialog.open was
called; update it to assert the actual dialog component and configuration passed
to dialog.open: verify that dialog.open was invoked with the
CreateHostedDatabaseDialogComponent (or the specific dialog class used when
creating a hosted DB) and an options object whose data contains the company id
('company-id') (and any relevant flags like disableClose or width if used).
Replace the generic expect(dialog.open).toHaveBeenCalled() with an assertion
that inspects the first and second arguments passed to dialog.open to ensure the
component and data payload are correct.
In `@frontend/src/app/services/hosted-database.service.ts`:
- Around line 11-13: The return type Promise<CreatedHostedDatabase | null> is
misleading because createHostedDatabase simply returns this._api.post(...) which
throws on errors; remove the nullable union and change the signature to
Promise<CreatedHostedDatabase> (or, alternatively, if you intend to return null
for specific API responses, catch errors from this._api.post in
createHostedDatabase, inspect the response/status and explicitly return null for
those cases and rethrow for others); update callers accordingly. Ensure you
modify the createHostedDatabase method signature and either remove the | null or
add the explicit try/catch handling inside createHostedDatabase to return null
for the intended conditions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 509c2bf4-d543-46b2-abdb-b393e3096e31
📒 Files selected for processing (15)
frontend/src/app/components/connections-list/hosted-database-plan-dialog/hosted-database-plan-dialog.component.cssfrontend/src/app/components/connections-list/hosted-database-plan-dialog/hosted-database-plan-dialog.component.htmlfrontend/src/app/components/connections-list/hosted-database-plan-dialog/hosted-database-plan-dialog.component.tsfrontend/src/app/components/connections-list/hosted-database-success-dialog/hosted-database-success-dialog.component.cssfrontend/src/app/components/connections-list/hosted-database-success-dialog/hosted-database-success-dialog.component.htmlfrontend/src/app/components/connections-list/hosted-database-success-dialog/hosted-database-success-dialog.component.tsfrontend/src/app/components/connections-list/own-connections/own-connections.component.cssfrontend/src/app/components/connections-list/own-connections/own-connections.component.htmlfrontend/src/app/components/connections-list/own-connections/own-connections.component.spec.tsfrontend/src/app/components/connections-list/own-connections/own-connections.component.tsfrontend/src/app/components/upgrade/upgrade.component.htmlfrontend/src/app/components/upgrade/upgrade.component.tsfrontend/src/app/models/hosted-database.tsfrontend/src/app/services/hosted-database.service.spec.tsfrontend/src/app/services/hosted-database.service.ts
| get credentialsText(): string { | ||
| const { username, password, hostname, port, databaseName } = this.data.hostedDatabase; | ||
| return `postgres://${username}:${password}@${hostname}:${port}/${databaseName}`; | ||
| } |
There was a problem hiding this comment.
URL-encode credentials in the connection string.
If the password contains special characters (e.g., @, :, /, #, ?), the connection URI will be malformed and unusable. Consider URL-encoding the username and password.
🛠️ Proposed fix
get credentialsText(): string {
const { username, password, hostname, port, databaseName } = this.data.hostedDatabase;
- return `postgres://${username}:${password}@${hostname}:${port}/${databaseName}`;
+ return `postgres://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${hostname}:${port}/${databaseName}`;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| get credentialsText(): string { | |
| const { username, password, hostname, port, databaseName } = this.data.hostedDatabase; | |
| return `postgres://${username}:${password}@${hostname}:${port}/${databaseName}`; | |
| } | |
| get credentialsText(): string { | |
| const { username, password, hostname, port, databaseName } = this.data.hostedDatabase; | |
| return `postgres://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${hostname}:${port}/${databaseName}`; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@frontend/src/app/components/connections-list/hosted-database-success-dialog/hosted-database-success-dialog.component.ts`
around lines 28 - 31, The connection string built in the credentialsText getter
currently interpolates raw username and password and can break if they contain
reserved characters; update the credentialsText getter (credentialsText) to
URL-encode the username and password (from this.data.hostedDatabase.username and
.password) before interpolating them into the URI (use a standard encoder like
encodeURIComponent or the project's URL-encoding utility) while leaving
hostname, port and databaseName unchanged so the returned string is a valid
postgres URI.
| console.log('[HostedDB] subscriptionLevel:', subscriptionLevel, 'isFreePlan:', isFreePlan); | ||
|
|
||
| if (isFreePlan) { | ||
| const choice = await this._openPlanDialog(); | ||
| console.log('[HostedDB] plan dialog choice:', choice); |
There was a problem hiding this comment.
Remove debug console.log statements.
These debug statements should be removed before merging to production.
🧹 Proposed fix
const subscriptionLevel = this.currentUser.subscriptionLevel;
const isFreePlan = !subscriptionLevel || subscriptionLevel === SubscriptionPlans.free;
- console.log('[HostedDB] subscriptionLevel:', subscriptionLevel, 'isFreePlan:', isFreePlan);
if (isFreePlan) {
const choice = await this._openPlanDialog();
- console.log('[HostedDB] plan dialog choice:', choice);
if (!choice) {
return;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log('[HostedDB] subscriptionLevel:', subscriptionLevel, 'isFreePlan:', isFreePlan); | |
| if (isFreePlan) { | |
| const choice = await this._openPlanDialog(); | |
| console.log('[HostedDB] plan dialog choice:', choice); | |
| const subscriptionLevel = this.currentUser.subscriptionLevel; | |
| const isFreePlan = !subscriptionLevel || subscriptionLevel === SubscriptionPlans.free; | |
| if (isFreePlan) { | |
| const choice = await this._openPlanDialog(); | |
| if (!choice) { | |
| return; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@frontend/src/app/components/connections-list/own-connections/own-connections.component.ts`
around lines 116 - 120, Remove the debug console.log statements in
OwnConnectionsComponent around the free-plan check: delete the console.log that
logs '[HostedDB] subscriptionLevel:' and the one that logs '[HostedDB] plan
dialog choice:' (the lines referencing subscriptionLevel, isFreePlan and the
result of this._openPlanDialog()). Leave the existing logic (the isFreePlan
conditional and the await this._openPlanDialog() call) intact; if structured
logging is required, replace console.log with the app's logger (e.g.,
this.logger.debug) instead of console methods.
| if (!hostedDatabase) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Silent return when hostedDatabase is falsy leaves user without feedback.
If the API call succeeds but returns a falsy value, the user sees no success dialog, no error, and no indication of what happened. Consider either showing an error or logging this unexpected state.
🛠️ Proposed fix
const hostedDatabase = await this._hostedDatabaseService.createHostedDatabase(companyId);
if (!hostedDatabase) {
+ this._notifications.showAlert(AlertType.Error, 'Unexpected response from server. Please try again.', [
+ {
+ type: AlertActionType.Button,
+ caption: 'Dismiss',
+ action: (_id: number) => this._notifications.dismissAlert(),
+ },
+ ]);
return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!hostedDatabase) { | |
| return; | |
| } | |
| if (!hostedDatabase) { | |
| this._notifications.showAlert(AlertType.Error, 'Unexpected response from server. Please try again.', [ | |
| { | |
| type: AlertActionType.Button, | |
| caption: 'Dismiss', | |
| action: (_id: number) => this._notifications.dismissAlert(), | |
| }, | |
| ]); | |
| return; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@frontend/src/app/components/connections-list/own-connections/own-connections.component.ts`
around lines 138 - 140, The early silent return when hostedDatabase is falsy
hides an unexpected error from users; update the block that currently reads "if
(!hostedDatabase) { return; }" to instead log the unexpected falsy value (e.g.
console.error or your logger) and show a user-facing error notification/dialog
using the component's existing UI notification service (e.g.
this.notificationService.error(...), this.toastService.showError(...) or
this.dialogService.open(...)); only proceed to the success flow (show success
dialog/continue logic) when hostedDatabase is truthy so users get clear feedback
in both success and failure cases.
Summary by CodeRabbit
New Features
UI
Styles
Tests