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
226 changes: 203 additions & 23 deletions frontend/src/app/components/auto-configure/auto-configure.component.css
Original file line number Diff line number Diff line change
@@ -1,43 +1,223 @@
.auto-configure {
/* ── Page ── */

.page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 24px;
height: 100vh;
background: var(--color-primaryPalette-50);
}

.auto-configure__title {
color: var(--mat-sidenav-content-text-color);
margin: 0;
font-weight: 500;
@media (prefers-color-scheme: dark) {
.page {
background: var(--surface-dark-color);
}
}

.auto-configure__text {
color: var(--mat-sidenav-content-text-color);
opacity: 0.7;
/* ── Card ── */

.card {
width: 100%;
max-width: 480px;
border-radius: 16px;
background: var(--color-whitePalette-500);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.08),
0 4px 24px rgba(0, 0, 0, 0.06);
overflow: hidden;
}

@media (prefers-color-scheme: dark) {
.card {
background: var(--color-primaryPalette-900);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.3),
0 4px 24px rgba(0, 0, 0, 0.2);
}
}

@media (width <= 600px) {
.card {
margin: 0 16px;
}
}

.card__body {
display: flex;
flex-direction: column;
align-items: center;
padding: 48px 32px 36px;
gap: 16px;
}

.card__title {
margin: 0;
max-width: 400px;
font-size: 20px;
font-weight: 700;
color: var(--color-primaryPalette-900);
text-align: center;
}

.auto-configure__open-btn {
margin-top: 8px;
font-size: 16px;
padding: 0 32px;
@media (prefers-color-scheme: dark) {
.card__title {
color: var(--color-primaryPalette-50);
}
}

.auto-configure__hint {
color: var(--mat-sidenav-content-text-color);
opacity: 0.5;
.card__subtitle {
margin: 0;
font-size: 13px;
font-size: 14px;
color: var(--color-primaryPalette-400);
text-align: center;
line-height: 1.5;
max-width: 360px;
}

.auto-configure__notice {
color: var(--mat-sidenav-content-text-color);
opacity: 0.7;
@media (prefers-color-scheme: dark) {
.card__subtitle {
color: var(--color-primaryPalette-300);
}
}

.card__notice {
margin: 0;
font-size: 14px;
color: var(--color-primaryPalette-400);
text-align: center;
max-width: 400px;
max-width: 360px;
line-height: 1.5;
}

@media (prefers-color-scheme: dark) {
.card__notice {
color: var(--color-primaryPalette-300);
}
}

/* ── Footer ── */

.card__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 24px;
border-top: 1px solid var(--color-primaryPalette-200);
}

@media (prefers-color-scheme: dark) {
.card__footer {
border-top-color: var(--color-primaryPalette-800);
}
}

@media (width <= 600px) {
.card__footer {
flex-direction: column;
text-align: center;
}
}

.card__footer-text {
margin: 0;
font-size: 13px;
color: var(--color-primaryPalette-300);
line-height: 1.4;
flex: 1;
}

@media (prefers-color-scheme: dark) {
.card__footer-text {
color: var(--color-primaryPalette-400);
}
}

/* ── CTA button ── */

.card__cta {
flex-shrink: 0;
background: var(--color-accentedPalette-500) !important;
color: var(--color-accentedPalette-500-contrast) !important;
border-radius: 8px;
font-weight: 600;
font-size: 14px;
padding: 0 20px;
height: 40px;
text-decoration: none;
}

.card__cta:hover {
background: var(--color-accentedPalette-700) !important;
}

.card__cta-arrow {
margin-left: 6px;
}

/* ── Concentric spinning rings ── */

.spinner {
position: relative;
width: 72px;
height: 72px;
margin-bottom: 8px;
}

.spinner__ring {
position: absolute;
border-radius: 50%;
border: 3px solid transparent;
}

.spinner__ring--outer {
inset: 0;
border-top-color: var(--color-accentedPalette-500);
border-right-color: var(--color-accentedPalette-500);
animation: spin-outer 1.8s linear infinite;
}

.spinner__ring--inner {
inset: 10px;
border-bottom-color: var(--color-accentedPalette-500);
border-left-color: var(--color-accentedPalette-500);
animation: spin-inner 1.2s linear infinite;
}

@keyframes spin-outer {
to { transform: rotate(360deg); }
}

@keyframes spin-inner {
to { transform: rotate(-360deg); }
}

/* ── Indeterminate progress bar ── */

.progress-bar {
width: 100%;
max-width: 320px;
height: 4px;
border-radius: 2px;
background: var(--color-primaryPalette-200);
overflow: hidden;
margin-top: 4px;
}

@media (prefers-color-scheme: dark) {
.progress-bar {
background: var(--color-primaryPalette-800);
}
}

.progress-bar__fill {
width: 40%;
height: 100%;
border-radius: 2px;
background: var(--color-accentedPalette-500);
animation: indeterminate 1.5s ease-in-out infinite;
}

@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(350%); }
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
@if (loading()) {
<div class="auto-configure">
<mat-spinner diameter="48"></mat-spinner>
<h2 class="auto-configure__title">Setting up your dashboard</h2>
<p class="auto-configure__text">We're analyzing your database structure and applying the best configuration. This may take a while.</p>
<a mat-flat-button class="auto-configure__open-btn" [routerLink]="'/dashboard/' + connectionId()">Open tables</a>
<p class="auto-configure__hint">Setup will continue in the background</p>
<div class="page">
<div class="card">
<div class="card__body">
<div class="spinner">
<div class="spinner__ring spinner__ring--outer"></div>
<div class="spinner__ring spinner__ring--inner"></div>
</div>
<h2 class="card__title">Configuring your database</h2>
<p class="card__subtitle">We're analyzing your structure and applying the best settings. This is running in the background.</p>
<div class="progress-bar">
<div class="progress-bar__fill"></div>
</div>
</div>
<div class="card__footer">
<p class="card__footer-text">You can start working — setup will continue in the background.</p>
<a mat-flat-button class="card__cta" [routerLink]="'/dashboard/' + connectionId()">
Open tables
<span class="card__cta-arrow">&rarr;</span>
</a>
</div>
</div>
} @else if (errorMessage()) {
<div class="auto-configure">
<p class="auto-configure__notice">{{ errorMessage() }}</p>
<a mat-flat-button [routerLink]="'/dashboard/' + connectionId()">Continue to dashboard</a>
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -1,44 +1,31 @@
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';
import { ConfigurationStateService } from '../../services/configuration-state.service';

@Component({
selector: 'app-auto-configure',
templateUrl: './auto-configure.component.html',
styleUrls: ['./auto-configure.component.css'],
imports: [MatProgressSpinnerModule, MatButtonModule, RouterModule],
imports: [MatButtonModule, RouterModule],
})
export class AutoConfigureComponent implements OnInit {
private _route = inject(ActivatedRoute);
private _router = inject(Router);
private _api = inject(ApiService);
private _configState = inject(ConfigurationStateService);

protected connectionId = signal<string | null>(null);
protected loading = signal(true);
protected errorMessage = signal<string | null>(null);

ngOnInit(): void {
async ngOnInit(): Promise<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<void> {
const result = await this._api.get<string>(`/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.');
}
await this._configState.startConfiguring(connectionId);
this._router.navigate(['/dashboard', connectionId]);
}
Comment on lines +20 to 30
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

Guard against post-destruction navigation and consider checking the return value.

Two concerns with the current flow:

  1. Post-destruction navigation: If the user clicks "Open tables" and navigates to the dashboard before startConfiguring completes, this component is destroyed but the await continues. Line 29 will still execute after the promise resolves, potentially causing a redundant navigation or flicker.

  2. Ignored return value: startConfiguring returns false on failure (per ConfigurationStateService), but the component navigates unconditionally. If failed configurations should be handled differently (e.g., stay on page), this needs adjustment.

🛡️ Proposed fix to prevent post-destruction navigation
 export class AutoConfigureComponent implements OnInit {
 	private _route = inject(ActivatedRoute);
 	private _router = inject(Router);
 	private _configState = inject(ConfigurationStateService);
+	private _destroyed = false;

 	protected connectionId = signal<string | null>(null);

 	async ngOnInit(): Promise<void> {
 		const connectionId = this._route.snapshot.paramMap.get('connection-id');
 		if (!connectionId) {
 			this._router.navigate(['/connections-list']);
 			return;
 		}

 		this.connectionId.set(connectionId);
 		await this._configState.startConfiguring(connectionId);
-		this._router.navigate(['/dashboard', connectionId]);
+		if (!this._destroyed) {
+			this._router.navigate(['/dashboard', connectionId]);
+		}
 	}
+
+	ngOnDestroy(): void {
+		this._destroyed = true;
+	}
 }

Note: You'll need to add OnDestroy to the imports and implements clause.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/auto-configure/auto-configure.component.ts`
around lines 20 - 30, ngOnInit currently awaits
_configState.startConfiguring(connectionId) then unconditionally calls
_router.navigate which can run after the component is destroyed and ignores
startConfiguring's boolean result; implement OnDestroy and add a private
destroyed flag (or a Subject used with takeUntil) to guard post-destruction
work, await startConfiguring and check its return value (only navigate on true),
and before calling _router.navigate verify the component is not destroyed (e.g.,
if (!this._destroyed) or until-notification) to prevent redundant/flicker
navigation; update the class to implement OnDestroy and set the flag (or
complete the Subject) in ngOnDestroy.

}
Loading
Loading