Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.plan-dialog__content {
display: flex;
flex-direction: column;
gap: 12px;
min-width: min(100%, 30rem);
margin-top: 16px;
}

.plan-dialog__option {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 12px;
background: transparent;
cursor: pointer;
text-align: left;
transition:
border-color 0.15s,
background 0.15s;
}

.plan-dialog__option:hover {
border-color: var(--color-accentedPalette-200);
background: var(--color-accentedPalette-50);
}

.plan-dialog__option-header {
display: flex;
align-items: center;
gap: 8px;
color: var(--mat-sidenav-content-text-color);
}

.plan-dialog__option-title {
font-size: 16px;
font-weight: 600;
}

.plan-dialog__option-description {
margin: 0;
color: rgba(0, 0, 0, 0.64);
font-size: 14px;
}

.plan-dialog__option-price {
font-size: 13px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: rgba(0, 0, 0, 0.48);
}

@media (prefers-color-scheme: dark) {
.plan-dialog__option {
border-color: rgba(255, 255, 255, 0.08);
}

.plan-dialog__option:hover {
border-color: var(--color-accentedPalette-500);
background: var(--color-accentedPalette-700);
}

.plan-dialog__option-description {
color: rgba(255, 255, 255, 0.64);
}

.plan-dialog__option-price {
color: rgba(255, 255, 255, 0.48);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<h1 mat-dialog-title>Choose your hosted database plan</h1>

<mat-dialog-content class="plan-dialog__content">
<button type="button" class="plan-dialog__option" (click)="chooseFree()">
<div class="plan-dialog__option-header">
<mat-icon>dns</mat-icon>
<span class="plan-dialog__option-title">Tiny node</span>
</div>
<p class="plan-dialog__option-description">
Up to 0.1 CPU and 100 MB storage
</p>
<span class="plan-dialog__option-price">Free</span>
</button>

<button type="button" class="plan-dialog__option" (click)="chooseUpgrade()">
<div class="plan-dialog__option-header">
<mat-icon>rocket_launch</mat-icon>
<span class="plan-dialog__option-title">Scalable node</span>
</div>
<p class="plan-dialog__option-description">
$0.04/CPU/minute, $0.2/GB/month and $0.04/million IOPS
</p>
<span class="plan-dialog__option-price">Pay as you go</span>
</button>
</mat-dialog-content>

<mat-dialog-actions align="end">
<button type="button" mat-button mat-dialog-close>Cancel</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';

export type HostedDatabasePlanChoice = 'free' | 'upgrade';

@Component({
selector: 'app-hosted-database-plan-dialog',
templateUrl: './hosted-database-plan-dialog.component.html',
styleUrl: './hosted-database-plan-dialog.component.css',
imports: [MatDialogModule, MatButtonModule, MatIconModule],
})
export class HostedDatabasePlanDialogComponent {
constructor(private _dialogRef: MatDialogRef<HostedDatabasePlanDialogComponent, HostedDatabasePlanChoice>) {}

chooseFree(): void {
this._dialogRef.close('free');
}

chooseUpgrade(): void {
this._dialogRef.close('upgrade');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
.hosted-dialog__content {
display: flex;
flex-direction: column;
gap: 16px;
min-width: min(100%, 34rem);
}

.hosted-dialog__description,
.hosted-dialog__hint,
.hosted-dialog__error {
margin: 0;
}

.hosted-dialog__error {
background: var(--color-error-background);
border: 1px solid var(--color-error);
border-radius: 8px;
color: var(--color-error);
padding: 12px 14px;
}

.hosted-dialog__credentials {
display: grid;
gap: 10px;
border: 1px solid var(--color-accentedPalette-100);
border-radius: 12px;
padding: 14px;
}

.hosted-dialog__row {
display: grid;
grid-template-columns: minmax(96px, 120px) minmax(0, 1fr);
align-items: start;
gap: 12px;
}

.hosted-dialog__label {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}

.hosted-dialog__credentials code {
background: var(--color-accentedPalette-50);
border-radius: 8px;
font-family: "IBM Plex Mono", monospace;
padding: 8px 10px;
overflow-wrap: anywhere;
}

.hosted-dialog__hint {
color: rgba(0, 0, 0, 0.64);
font-size: 13px;
}

.hosted-dialog__actions {
gap: 8px;
padding-top: 8px;
}

@media (prefers-color-scheme: dark) {
.hosted-dialog__credentials {
background: transparent;
border-color: var(--color-accentedPalette-400);
}

.hosted-dialog__credentials code {
background: var(--color-accentedPalette-600);
}

.hosted-dialog__hint {
color: rgba(255, 255, 255, 0.64);
}
}

@media (width <= 600px) {
.hosted-dialog__content {
min-width: auto;
}

.hosted-dialog__row {
grid-template-columns: 1fr;
gap: 6px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<h1 mat-dialog-title>
{{ data.connectionId ? 'Hosted PostgreSQL is ready' : 'Hosted PostgreSQL created' }}
</h1>

<mat-dialog-content class="hosted-dialog__content">
@if (data.connectionId) {
<p class="hosted-dialog__description">
Your hosted PostgreSQL database is provisioned and already connected to RocketAdmin.
Save these credentials now. The password is shown only once.
</p>
} @else {
<p class="hosted-dialog__description">
Your hosted PostgreSQL database is provisioned, but RocketAdmin could not finish the automatic connection setup.
Save these credentials now and use them for a manual PostgreSQL connection or support follow-up.
</p>
}

@if (data.errorMessage) {
<p class="hosted-dialog__error">
Automatic connection setup failed: {{ data.errorMessage }}
</p>
}

<div class="hosted-dialog__credentials">
<div class="hosted-dialog__row">
<span class="hosted-dialog__label">Database</span>
<code>{{ data.hostedDatabase.databaseName }}</code>
</div>
<div class="hosted-dialog__row">
<span class="hosted-dialog__label">Host</span>
<code>{{ data.hostedDatabase.hostname }}</code>
</div>
<div class="hosted-dialog__row">
<span class="hosted-dialog__label">Port</span>
<code>{{ data.hostedDatabase.port }}</code>
</div>
<div class="hosted-dialog__row">
<span class="hosted-dialog__label">Username</span>
<code>{{ data.hostedDatabase.username }}</code>
</div>
<div class="hosted-dialog__row">
<span class="hosted-dialog__label">Password</span>
<code>{{ data.hostedDatabase.password }}</code>
</div>
</div>

<p class="hosted-dialog__hint">
The generated password cannot be recovered from this screen later.
</p>
</mat-dialog-content>

<mat-dialog-actions align="end" class="hosted-dialog__actions">
<button type="button" mat-stroked-button mat-dialog-close>
Close
</button>
<button
type="button"
mat-flat-button
color="accent"
[cdkCopyToClipboard]="credentialsText"
(cdkCopyToClipboardCopied)="handleCredentialsCopied()">
Copy credentials
</button>
@if (data.connectionId) {
<a
mat-button
[routerLink]="['/dashboard', data.connectionId]"
mat-dialog-close
(click)="handleSecondaryActionClick()">
Open tables
</a>
<a
mat-flat-button
color="primary"
[routerLink]="['/auto-configure', data.connectionId]"
mat-dialog-close
cdkFocusInitial
(click)="handlePrimaryActionClick()">
Set up dashboard
</a>
}
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CdkCopyToClipboard } from '@angular/cdk/clipboard';
import { Component, Inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router';
import posthog from 'posthog-js';
import { CreatedHostedDatabase } from 'src/app/models/hosted-database';
import { NotificationsService } from 'src/app/services/notifications.service';

export interface HostedDatabaseSuccessDialogData {
hostedDatabase: CreatedHostedDatabase;
connectionId: string | null;
errorMessage?: string;
}

@Component({
selector: 'app-hosted-database-success-dialog',
templateUrl: './hosted-database-success-dialog.component.html',
styleUrl: './hosted-database-success-dialog.component.css',
imports: [MatDialogModule, MatButtonModule, RouterModule, CdkCopyToClipboard],
})
export class HostedDatabaseSuccessDialogComponent {
constructor(
@Inject(MAT_DIALOG_DATA) public data: HostedDatabaseSuccessDialogData,
private _notifications: NotificationsService,
) {}

get credentialsText(): string {
const { username, password, hostname, port, databaseName } = this.data.hostedDatabase;
return `postgres://${username}:${password}@${hostname}:${port}/${databaseName}`;
}
Comment on lines +28 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.


handleCredentialsCopied(): void {
posthog.capture('Connections: hosted PostgreSQL credentials copied');
this._notifications.showSuccessSnackbar('Hosted database credentials were copied to clipboard.');
}

handlePrimaryActionClick(): void {
posthog.capture('Connections: hosted PostgreSQL setup dashboard opened');
}

handleSecondaryActionClick(): void {
posthog.capture('Connections: hosted PostgreSQL tables opened');
}
}
Loading
Loading