diff --git a/frontend/src/app/components/auto-configure/auto-configure.component.css b/frontend/src/app/components/auto-configure/auto-configure.component.css index b2352a8fd..aa84d8a0b 100644 --- a/frontend/src/app/components/auto-configure/auto-configure.component.css +++ b/frontend/src/app/components/auto-configure/auto-configure.component.css @@ -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%); } } diff --git a/frontend/src/app/components/auto-configure/auto-configure.component.html b/frontend/src/app/components/auto-configure/auto-configure.component.html index 14e2b4341..7e818bf08 100644 --- a/frontend/src/app/components/auto-configure/auto-configure.component.html +++ b/frontend/src/app/components/auto-configure/auto-configure.component.html @@ -1,14 +1,22 @@ -@if (loading()) { -
- -

Setting up your dashboard

-

We're analyzing your database structure and applying the best configuration. This may take a while.

- Open tables -

Setup will continue in the background

+
+
+
+
+
+
+
+

Configuring your database

+

We're analyzing your structure and applying the best settings. This is running in the background.

+
+
+
+
+
-} @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 index 322ee6548..c5b2c638c 100644 --- a/frontend/src/app/components/auto-configure/auto-configure.component.ts +++ b/frontend/src/app/components/auto-configure/auto-configure.component.ts @@ -1,26 +1,23 @@ 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(null); - protected loading = signal(true); - protected errorMessage = signal(null); - ngOnInit(): void { + async ngOnInit(): Promise { const connectionId = this._route.snapshot.paramMap.get('connection-id'); if (!connectionId) { this._router.navigate(['/connections-list']); @@ -28,17 +25,7 @@ export class AutoConfigureComponent implements OnInit { } 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.'); - } + await this._configState.startConfiguring(connectionId); + this._router.navigate(['/dashboard', connectionId]); } } diff --git a/frontend/src/app/components/dashboard/dashboard.component.css b/frontend/src/app/components/dashboard/dashboard.component.css index ae1ccbe98..17145b767 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.css +++ b/frontend/src/app/components/dashboard/dashboard.component.css @@ -180,6 +180,118 @@ } } +/* ── Status banner ── */ + +.status-banner { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 20px; + width: 100%; + border: none; + border-left: 3px solid var(--color-accentedPalette-500); + border-radius: 0; + background: var(--color-accentedPalette-50); + font-size: 13px; +} + +.status-banner__icon { + flex-shrink: 0; + color: var(--color-accentedPalette-500); + font-size: 20px; + width: 20px; + height: 20px; +} + +.status-banner__body { + flex: 1; + display: flex; + align-items: baseline; + gap: 6px; + min-width: 0; +} + +.status-banner__title { + color: var(--color-accentedPalette-900); + white-space: nowrap; +} + +.status-banner__subtitle { + color: var(--color-accentedPalette-700); +} + +.status-banner__link { + color: var(--color-accentedPalette-500); + text-decoration: underline; +} + +.status-banner__link:hover { + color: var(--color-accentedPalette-700); +} + +.status-banner__progress { + flex-shrink: 0; + width: 80px; + height: 3px; + border-radius: 2px; + background: var(--color-accentedPalette-100); + overflow: hidden; +} + +.status-banner__progress-fill { + width: 40%; + height: 100%; + border-radius: 2px; + background: var(--color-accentedPalette-500); + animation: banner-indeterminate 1.5s ease-in-out infinite; +} + +@keyframes banner-indeterminate { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(350%); } +} + +@media (prefers-color-scheme: dark) { + .status-banner { + background: var(--color-accentedPalette-900); + } + + .status-banner__title { + color: var(--color-accentedPalette-100); + } + + .status-banner__subtitle { + color: var(--color-accentedPalette-200); + } + + .status-banner__link { + color: var(--color-accentedPalette-500); + } + + .status-banner__link:hover { + color: var(--color-accentedPalette-200); + } + + .status-banner__icon { + color: var(--color-accentedPalette-500); + } + + .status-banner__progress { + background: var(--color-accentedPalette-800); + } + + .status-banner__progress-fill { + background: var(--color-accentedPalette-300); + } +} + +@media (width <= 600px) { + .status-banner__body { + flex-direction: column; + gap: 2px; + } +} + .error-details { margin-top: 8px; } diff --git a/frontend/src/app/components/dashboard/dashboard.component.html b/frontend/src/app/components/dashboard/dashboard.component.html index 4b2cca36f..800bcada7 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.html +++ b/frontend/src/app/components/dashboard/dashboard.component.html @@ -43,6 +43,21 @@

Rocketadmin can not find any tables

+ @if (isConfiguring) { +
+ info +
+ Configuring tables + Some tables may look incomplete while setup is running. You can adjust column types, display formats and filters in + Settings and + UI widgets after setup finishes. +
+
+
+
+
+ } + Rocketadmin can not find any tables
-
+
auto_awesome
New: AI Configuration @@ -92,6 +107,7 @@

Rocketadmin can not find any tables

Configure all
+
diff --git a/frontend/src/app/components/dashboard/dashboard.component.ts b/frontend/src/app/components/dashboard/dashboard.component.ts index 56cfdbf82..4179439f3 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.ts +++ b/frontend/src/app/components/dashboard/dashboard.component.ts @@ -23,6 +23,7 @@ import { TableRowService } from 'src/app/services/table-row.service'; import { TableStateService } from 'src/app/services/table-state.service'; import { TablesService } from 'src/app/services/tables.service'; import { UiSettingsService } from 'src/app/services/ui-settings.service'; +import { ConfigurationStateService } from 'src/app/services/configuration-state.service'; import { environment } from 'src/environments/environment'; import { normalizeTableName } from '../../lib/normalize'; import { PlaceholderTableViewComponent } from '../skeletons/placeholder-table-view/placeholder-table-view.component'; @@ -99,6 +100,7 @@ export class DashboardComponent implements OnInit, OnDestroy { public uiSettings: ConnectionSettingsUI; public tableFolders: any[] = []; + public isConfiguring: boolean = false; constructor( private _connections: ConnectionsService, @@ -107,6 +109,7 @@ export class DashboardComponent implements OnInit, OnDestroy { private _uiSettings: UiSettingsService, private _tableState: TableStateService, private _company: CompanyService, + private _configState: ConfigurationStateService, public router: Router, private route: ActivatedRoute, public dialog: MatDialog, @@ -133,6 +136,10 @@ export class DashboardComponent implements OnInit, OnDestroy { ngOnInit() { this.connectionID = this._connections.currentConnectionID; this.dataSource = new TablesDataSource(this._tables, this._connections, this._tableRow); + + // Check if auto-configure is running for this connection + this.isConfiguring = this._configState.isConfiguring(this.connectionID); + this._tableState.cast.subscribe((row) => { this.selectedRow = row; }); diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.css b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.css index 9a6d23893..cdce0f075 100644 --- a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.css +++ b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.css @@ -591,29 +591,6 @@ max-width: calc(100% - 40px); } -.folder-name.editing { - background: white; - border: 1px solid #2196f3; - border-radius: 2px; - padding: 2px 6px; - margin: 0 4px; - flex: 1; - max-width: 100%; -} - -.enter-icon { - font-size: 16px; - width: 16px; - height: 16px; - color: #9e9e9e; - cursor: pointer; - margin-left: 4px; -} - -.enter-icon:hover { - color: #757575; -} - .folder-icon { font-size: 20px; width: 20px; @@ -708,7 +685,8 @@ .table-link { flex: 1; - display: block; + display: flex; + align-items: center; text-decoration: none; color: inherit; overflow: hidden; @@ -722,6 +700,12 @@ padding: 0 12px; } +.table-link_active { + border-radius: 4px; + margin: 0 -12px; + padding: 0 12px; +} + .table-link_active .table-name { color: var(--color-accentedPalette-500); } diff --git a/frontend/src/app/models/table.ts b/frontend/src/app/models/table.ts index d4ec10a1a..3ba61dc9a 100644 --- a/frontend/src/app/models/table.ts +++ b/frontend/src/app/models/table.ts @@ -13,6 +13,8 @@ export interface TableProperties { initials?: string; icon?: string; permissions: TablePermissions; + configured?: boolean; + configuring?: boolean; } export enum TableOrdering { diff --git a/frontend/src/app/services/configuration-state.service.ts b/frontend/src/app/services/configuration-state.service.ts new file mode 100644 index 000000000..55268e4f2 --- /dev/null +++ b/frontend/src/app/services/configuration-state.service.ts @@ -0,0 +1,43 @@ +import { inject, Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +import { ApiService } from './api.service'; +import { NotificationsService } from './notifications.service'; + +@Injectable({ providedIn: 'root' }) +export class ConfigurationStateService { + private _api = inject(ApiService); + private _notifications = inject(NotificationsService); + private _configuring = new BehaviorSubject>(new Set()); + + /** Start configuring — fires the API and manages state. Returns a promise that resolves when setup completes. */ + startConfiguring(connectionId: string): Promise { + if (this._configuring.value.has(connectionId)) return Promise.resolve(false); + + const current = this._configuring.value; + current.add(connectionId); + this._configuring.next(current); + + return this._runSetup(connectionId); + } + + /** Check if a connection is currently being configured */ + isConfiguring(connectionId: string): boolean { + return this._configuring.value.has(connectionId); + } + + private async _runSetup(connectionId: string): Promise { + try { + await this._api.get(`/ai/v2/setup/${connectionId}`, { responseType: 'text' }); + this._notifications.showSuccessSnackbar('All tables have been configured.'); + return true; + } catch { + this._notifications.showSuccessSnackbar('Configuration could not be completed.'); + return false; + } finally { + const current = this._configuring.value; + current.delete(connectionId); + this._configuring.next(current); + } + } +}