-
-
Notifications
You must be signed in to change notification settings - Fork 18
Add hosted databases management page with delete and reset password d… #1684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .mat-mdc-dialog-content { | ||
| margin-bottom: -20px; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <h1 mat-dialog-title>Delete <strong>{{ data.databaseName }}</strong></h1> | ||
| <mat-dialog-content> | ||
| <p class="mat-body"> | ||
| This will permanently delete the hosted database and all its data. | ||
| This action cannot be undone. | ||
| </p> | ||
| </mat-dialog-content> | ||
| <mat-dialog-actions align="end"> | ||
| <button type="button" mat-flat-button mat-dialog-close>Cancel</button> | ||
| <button type="button" mat-flat-button color="warn" | ||
| [disabled]="submitting()" | ||
| (click)="deleteDatabase()"> | ||
| {{ submitting() ? 'Deleting...' : 'Delete' }} | ||
| </button> | ||
| </mat-dialog-actions> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,38 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Component, inject, signal } from '@angular/core'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { MatButtonModule } from '@angular/material/button'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import posthog from 'posthog-js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { FoundHostedDatabase } from 'src/app/models/hosted-database'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { HostedDatabaseService } from 'src/app/services/hosted-database.service'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NotificationsService } from 'src/app/services/notifications.service'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Component({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selector: 'app-hosted-database-delete-dialog', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| templateUrl: './hosted-database-delete-dialog.component.html', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| styleUrls: ['./hosted-database-delete-dialog.component.css'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imports: [MatDialogModule, MatButtonModule], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class HostedDatabaseDeleteDialogComponent { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private _hostedDatabaseService = inject(HostedDatabaseService); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private _notifications = inject(NotificationsService); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private _dialogRef = inject(MatDialogRef<HostedDatabaseDeleteDialogComponent>); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protected data: FoundHostedDatabase = inject(MAT_DIALOG_DATA); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protected submitting = signal(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async deleteDatabase(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.submitting.set(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await this._hostedDatabaseService.deleteHostedDatabase(this.data.companyId, this.data.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result?.success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| posthog.capture('Hosted Databases: database deleted', { databaseName: this.data.databaseName }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this._notifications.showSuccessSnackbar('Hosted database deleted successfully.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this._dialogRef.close('delete'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.submitting.set(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error feedback when deletion fails. If 🛡️ Proposed fix to add error handling async deleteDatabase(): Promise<void> {
this.submitting.set(true);
try {
const result = await this._hostedDatabaseService.deleteHostedDatabase(this.data.companyId, this.data.id);
if (result?.success) {
posthog.capture('Hosted Databases: database deleted', { databaseName: this.data.databaseName });
this._notifications.showSuccessSnackbar('Hosted database deleted successfully.');
this._dialogRef.close('delete');
+ } else {
+ this._notifications.showErrorSnackbar('Failed to delete database. Please try again.');
}
+ } catch {
+ this._notifications.showErrorSnackbar('Failed to delete database. Please try again.');
} finally {
this.submitting.set(false);
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| .reset-dialog__content { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 16px; | ||
| min-width: min(100%, 34rem); | ||
| } | ||
|
|
||
| .reset-dialog__credentials { | ||
| display: grid; | ||
| gap: 10px; | ||
| border: 1px solid var(--color-accentedPalette-100); | ||
| border-radius: 12px; | ||
| padding: 14px; | ||
| } | ||
|
|
||
| .reset-dialog__row { | ||
| display: grid; | ||
| grid-template-columns: minmax(96px, 120px) minmax(0, 1fr); | ||
| align-items: start; | ||
| gap: 12px; | ||
| } | ||
|
|
||
| .reset-dialog__label { | ||
| font-size: 12px; | ||
| font-weight: 700; | ||
| letter-spacing: 0.04em; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| .reset-dialog__credentials code { | ||
| background: var(--color-accentedPalette-50); | ||
| border-radius: 8px; | ||
| font-family: "IBM Plex Mono", monospace; | ||
| padding: 8px 10px; | ||
| overflow-wrap: anywhere; | ||
| } | ||
|
|
||
| .reset-dialog__hint { | ||
| color: rgba(0, 0, 0, 0.64); | ||
| font-size: 13px; | ||
| margin: 0; | ||
| } | ||
|
|
||
| .reset-dialog__actions { | ||
| gap: 8px; | ||
| padding-top: 8px; | ||
| } | ||
|
|
||
| @media (prefers-color-scheme: dark) { | ||
| .reset-dialog__credentials { | ||
| background: transparent; | ||
| border-color: var(--color-accentedPalette-400); | ||
| } | ||
|
|
||
| .reset-dialog__credentials code { | ||
| background: var(--color-accentedPalette-600); | ||
| } | ||
|
|
||
| .reset-dialog__hint { | ||
| color: rgba(255, 255, 255, 0.64); | ||
| } | ||
| } | ||
|
|
||
| @media (width <= 600px) { | ||
| .reset-dialog__content { | ||
| min-width: auto; | ||
| } | ||
|
|
||
| .reset-dialog__row { | ||
| grid-template-columns: 1fr; | ||
| gap: 6px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| @if (phase() === 'confirm') { | ||
| <h1 mat-dialog-title>Reset password for <strong>{{ data.databaseName }}</strong></h1> | ||
| <mat-dialog-content> | ||
| <p class="mat-body"> | ||
| This will generate a new password for the database. | ||
| Any existing connections using the current password will stop working. | ||
| </p> | ||
| </mat-dialog-content> | ||
| <mat-dialog-actions align="end"> | ||
| <button type="button" mat-flat-button mat-dialog-close>Cancel</button> | ||
| <button type="button" mat-flat-button color="warn" | ||
| [disabled]="submitting()" | ||
| (click)="resetPassword()"> | ||
| {{ submitting() ? 'Resetting...' : 'Reset Password' }} | ||
| </button> | ||
| </mat-dialog-actions> | ||
| } @else { | ||
| <h1 mat-dialog-title>New password for <strong>{{ result()!.databaseName }}</strong></h1> | ||
| <mat-dialog-content class="reset-dialog__content"> | ||
| <div class="reset-dialog__credentials"> | ||
| <div class="reset-dialog__row"> | ||
| <span class="reset-dialog__label">Host</span> | ||
| <code>{{ result()!.hostname }}</code> | ||
| </div> | ||
| <div class="reset-dialog__row"> | ||
| <span class="reset-dialog__label">Port</span> | ||
| <code>{{ result()!.port }}</code> | ||
| </div> | ||
| <div class="reset-dialog__row"> | ||
| <span class="reset-dialog__label">Username</span> | ||
| <code>{{ result()!.username }}</code> | ||
| </div> | ||
| <div class="reset-dialog__row"> | ||
| <span class="reset-dialog__label">Password</span> | ||
| <code>{{ result()!.password }}</code> | ||
| </div> | ||
| </div> | ||
| <p class="reset-dialog__hint"> | ||
| Save the new password now. It cannot be recovered from this screen later. | ||
| </p> | ||
| </mat-dialog-content> | ||
| <mat-dialog-actions align="end" class="reset-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> | ||
| </mat-dialog-actions> | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||||||
| import { CdkCopyToClipboard } from '@angular/cdk/clipboard'; | ||||||||||
| import { Component, computed, inject, signal } from '@angular/core'; | ||||||||||
| import { MatButtonModule } from '@angular/material/button'; | ||||||||||
| import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; | ||||||||||
| import posthog from 'posthog-js'; | ||||||||||
| import { CreatedHostedDatabase, FoundHostedDatabase } from 'src/app/models/hosted-database'; | ||||||||||
| import { HostedDatabaseService } from 'src/app/services/hosted-database.service'; | ||||||||||
| import { NotificationsService } from 'src/app/services/notifications.service'; | ||||||||||
|
|
||||||||||
| @Component({ | ||||||||||
| selector: 'app-hosted-database-reset-password-dialog', | ||||||||||
| templateUrl: './hosted-database-reset-password-dialog.component.html', | ||||||||||
| styleUrls: ['./hosted-database-reset-password-dialog.component.css'], | ||||||||||
| imports: [MatDialogModule, MatButtonModule, CdkCopyToClipboard], | ||||||||||
| }) | ||||||||||
| export class HostedDatabaseResetPasswordDialogComponent { | ||||||||||
| private _hostedDatabaseService = inject(HostedDatabaseService); | ||||||||||
| private _notifications = inject(NotificationsService); | ||||||||||
|
|
||||||||||
| protected data: FoundHostedDatabase = inject(MAT_DIALOG_DATA); | ||||||||||
| protected submitting = signal(false); | ||||||||||
| protected result = signal<CreatedHostedDatabase | null>(null); | ||||||||||
| protected phase = computed(() => (this.result() ? 'result' : 'confirm')); | ||||||||||
|
|
||||||||||
| get credentialsText(): string { | ||||||||||
| const r = this.result(); | ||||||||||
| if (!r) return ''; | ||||||||||
| return `postgres://${r.username}:${r.password}@${r.hostname}:${r.port}/${r.databaseName}`; | ||||||||||
|
||||||||||
| return `postgres://${r.username}:${r.password}@${r.hostname}:${r.port}/${r.databaseName}`; | |
| const username = encodeURIComponent(r.username); | |
| const password = encodeURIComponent(r.password); | |
| return `postgres://${username}:${password}@${r.hostname}:${r.port}/${r.databaseName}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error feedback when password reset fails.
Similar to the delete dialog, if res is falsy (API failure or error), the user receives no feedback. Consider adding error notification.
🛡️ Proposed fix to add error handling
async resetPassword(): Promise<void> {
this.submitting.set(true);
try {
const res = await this._hostedDatabaseService.resetHostedDatabasePassword(this.data.companyId, this.data.id);
if (res) {
posthog.capture('Hosted Databases: password reset', { databaseName: this.data.databaseName });
this.result.set(res);
+ } else {
+ this._notifications.showErrorSnackbar('Failed to reset password. Please try again.');
}
+ } catch {
+ this._notifications.showErrorSnackbar('Failed to reset password. Please try again.');
} finally {
this.submitting.set(false);
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@frontend/src/app/components/hosted-databases/hosted-database-reset-password-dialog/hosted-database-reset-password-dialog.component.ts`
around lines 31 - 44, The resetPassword() method lacks user feedback when the
API returns a falsy response; update resetPassword() (and the branch around
_hostedDatabaseService.resetHostedDatabasePassword) to handle the else case:
when res is falsy, call the same UI notification mechanism used elsewhere (e.g.,
the delete dialog's error notifier) to show an error message to the user,
optionally capture a failure event with posthog.capture, and ensure this.result
is not set on failure; keep the existing submitting.set(false) in the finally
block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HostedDatabaseDeleteDialogComponent only closes the dialog when
result?.successis truthy; if the API returns{ success: false }(a valid response per the method’s return type), the dialog stays open with no feedback to the user. Consider handling the non-success case explicitly (e.g., show an error snackbar/alert and keep the dialog actionable, or close with a failure result).