diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 12966d378..0ed8b0dab 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -227,14 +227,14 @@ const routes: Routes = [ { path: 'panels/:connection-id/new', loadComponent: () => - import('./components/charts/chart-edit/chart-edit.component').then((m) => m.ChartEditComponent), + import('./components/charts/panel-edit/panel-edit.component').then((m) => m.PanelEditComponent), canActivate: [AuthGuard], title: 'Create Query | Rocketadmin', }, { path: 'panels/:connection-id/:query-id', loadComponent: () => - import('./components/charts/chart-edit/chart-edit.component').then((m) => m.ChartEditComponent), + import('./components/charts/panel-edit/panel-edit.component').then((m) => m.PanelEditComponent), canActivate: [AuthGuard], title: 'Edit Query | Rocketadmin', }, diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 04b32ee9a..b9e1daba2 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -130,11 +130,11 @@ export class AppComponent { ); this.matIconRegistry.addSvgIcon( 'cassandra', - this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/icons/db-logos/сassandra_logo.svg'), + this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/icons/db-logos/cassandra_logo.svg'), ); this.matIconRegistry.addSvgIcon( 'cassandra-dark', - this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/icons/db-logos/сassandra_logo_dark.svg'), + this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/icons/db-logos/cassandra_logo_dark.svg'), ); this.matIconRegistry.addSvgIcon( 'redis', diff --git a/frontend/src/app/components/charts/chart-edit/chart-edit.component.css b/frontend/src/app/components/charts/panel-edit/panel-edit.component.css similarity index 98% rename from frontend/src/app/components/charts/chart-edit/chart-edit.component.css rename to frontend/src/app/components/charts/panel-edit/panel-edit.component.css index 2498f2107..86cc87441 100644 --- a/frontend/src/app/components/charts/chart-edit/chart-edit.component.css +++ b/frontend/src/app/components/charts/panel-edit/panel-edit.component.css @@ -7,19 +7,19 @@ z-index: 1; } -.chart-edit-layout { +.panel-edit-layout { display: flex; height: 100%; } -.chart-edit-main { +.panel-edit-main { flex: 1; overflow-y: auto; display: flex; flex-direction: column; } -.chart-edit-page { +.panel-edit-page { display: flex; flex-direction: column; flex: 1; @@ -31,13 +31,13 @@ } @media (width <= 600px) { - .chart-edit-page { + .panel-edit-page { padding: 0 16px; margin: 1.5em auto; } } -.chart-edit-header { +.panel-edit-header { display: flex; align-items: center; gap: 16px; @@ -45,7 +45,7 @@ flex-shrink: 0; } -.chart-edit-header h1 { +.panel-edit-header h1 { margin: 0; } @@ -55,7 +55,7 @@ padding: 64px; } -.chart-edit-content { +.panel-edit-content { display: flex; flex-direction: column; gap: 24px; diff --git a/frontend/src/app/components/charts/chart-edit/chart-edit.component.html b/frontend/src/app/components/charts/panel-edit/panel-edit.component.html similarity index 99% rename from frontend/src/app/components/charts/chart-edit/chart-edit.component.html rename to frontend/src/app/components/charts/panel-edit/panel-edit.component.html index 866ab4b55..dc7e587ab 100644 --- a/frontend/src/app/components/charts/chart-edit/chart-edit.component.html +++ b/frontend/src/app/components/charts/panel-edit/panel-edit.component.html @@ -1,14 +1,14 @@ -
+
-
+
-
-
+
+

{{ isEditMode() ? 'Edit panel' : 'Create panel' }}

@@ -19,7 +19,7 @@

{{ isEditMode() ? 'Edit panel' : 'Create panel' }}

} @if (!loading()) { -
+
@if (aiTables().length > 0 && aiDashboardId()) {
diff --git a/frontend/src/app/components/charts/chart-edit/chart-edit.component.spec.ts b/frontend/src/app/components/charts/panel-edit/panel-edit.component.spec.ts similarity index 84% rename from frontend/src/app/components/charts/chart-edit/chart-edit.component.spec.ts rename to frontend/src/app/components/charts/panel-edit/panel-edit.component.spec.ts index 994f01bfa..c36382991 100644 --- a/frontend/src/app/components/charts/chart-edit/chart-edit.component.spec.ts +++ b/frontend/src/app/components/charts/panel-edit/panel-edit.component.spec.ts @@ -14,9 +14,9 @@ import { ConnectionsService } from 'src/app/services/connections.service'; import { SavedQueriesService } from 'src/app/services/saved-queries.service'; import { UiSettingsService } from 'src/app/services/ui-settings.service'; import { MockCodeEditorComponent } from 'src/app/testing/code-editor.mock'; -import { ChartEditComponent } from './chart-edit.component'; +import { PanelEditComponent } from './panel-edit.component'; -type ChartEditComponentTestable = ChartEditComponent & { +type PanelEditComponentTestable = PanelEditComponent & { isEditMode: WritableSignal; queryName: WritableSignal; queryText: WritableSignal; @@ -34,9 +34,9 @@ type ChartEditComponentTestable = ChartEditComponent & { hasChartData: Signal; }; -describe('ChartEditComponent', () => { - let component: ChartEditComponent; - let fixture: ComponentFixture; +describe('PanelEditComponent', () => { + let component: PanelEditComponent; + let fixture: ComponentFixture; let mockSavedQueriesService: Partial; let mockConnectionsService: Partial; let mockUiSettingsService: Partial; @@ -77,7 +77,7 @@ describe('ChartEditComponent', () => { await TestBed.configureTestingModule({ imports: [ - ChartEditComponent, + PanelEditComponent, BrowserAnimationsModule, MatSnackBarModule, RouterTestingModule, @@ -105,7 +105,7 @@ describe('ChartEditComponent', () => { }, ], }) - .overrideComponent(ChartEditComponent, { + .overrideComponent(PanelEditComponent, { remove: { imports: [CodeEditorModule] }, add: { imports: [MockCodeEditorComponent], schemas: [NO_ERRORS_SCHEMA] }, }) @@ -114,7 +114,7 @@ describe('ChartEditComponent', () => { router = TestBed.inject(Router); vi.spyOn(router, 'navigate'); - fixture = TestBed.createComponent(ChartEditComponent); + fixture = TestBed.createComponent(PanelEditComponent); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -124,7 +124,7 @@ describe('ChartEditComponent', () => { }); it('should initialize in create mode when no query-id', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; expect(testable.isEditMode()).toBe(false); }); @@ -133,27 +133,27 @@ describe('ChartEditComponent', () => { }); it('should have correct default chart type', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; expect(testable.chartType()).toBe('bar'); }); describe('canSave computed', () => { it('should return false when name is empty', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryName.set(''); testable.queryText.set('SELECT 1'); expect(testable.canSave()).toBe(false); }); it('should return false when query is empty', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryName.set('Test'); testable.queryText.set(''); expect(testable.canSave()).toBe(false); }); it('should return true when name and query are provided', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryName.set('Test'); testable.queryText.set('SELECT 1'); testable.saving.set(false); @@ -161,7 +161,7 @@ describe('ChartEditComponent', () => { }); it('should return false when saving', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryName.set('Test'); testable.queryText.set('SELECT 1'); testable.saving.set(true); @@ -171,20 +171,20 @@ describe('ChartEditComponent', () => { describe('canTest computed', () => { it('should return false when query is empty', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryText.set(''); expect(testable.canTest()).toBe(false); }); it('should return true when query is provided', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryText.set('SELECT 1'); testable.testing.set(false); expect(testable.canTest()).toBe(true); }); it('should return false when testing', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryText.set('SELECT 1'); testable.testing.set(true); expect(testable.canTest()).toBe(false); @@ -193,7 +193,7 @@ describe('ChartEditComponent', () => { describe('testQuery', () => { it('should call testQuery service method', async () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryText.set('SELECT * FROM users'); await component.testQuery(); expect(mockSavedQueriesService.testQuery).toHaveBeenCalledWith('conn-1', { @@ -202,7 +202,7 @@ describe('ChartEditComponent', () => { }); it('should set results and columns after successful test', async () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryText.set('SELECT * FROM users'); await component.testQuery(); expect(testable.testResults()).toEqual([{ name: 'John', count: 10 }]); @@ -211,7 +211,7 @@ describe('ChartEditComponent', () => { }); it('should auto-select label column and first series', async () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryText.set('SELECT * FROM users'); await component.testQuery(); expect(testable.labelColumn()).toBe('name'); @@ -221,7 +221,7 @@ describe('ChartEditComponent', () => { describe('saveQuery', () => { it('should call createSavedQuery in create mode', async () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.isEditMode.set(false); testable.queryName.set('New Query'); testable.queryText.set('SELECT 1'); @@ -237,7 +237,7 @@ describe('ChartEditComponent', () => { }); it('should navigate to charts list after save', async () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.queryName.set('New Query'); testable.queryText.set('SELECT 1'); await component.saveQuery(); @@ -247,7 +247,7 @@ describe('ChartEditComponent', () => { describe('onCodeChange', () => { it('should update queryText', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; component.onCodeChange('SELECT * FROM table'); expect(testable.queryText()).toBe('SELECT * FROM table'); }); @@ -255,13 +255,13 @@ describe('ChartEditComponent', () => { describe('hasChartData computed', () => { it('should return false when no results', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.testResults.set([]); expect(testable.hasChartData()).toBe(false); }); it('should return false when no series configured', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.testResults.set([{ name: 'John' }]); testable.labelColumn.set('name'); testable.seriesList.set([]); @@ -269,7 +269,7 @@ describe('ChartEditComponent', () => { }); it('should return true when results, label and series are set', () => { - const testable = component as ChartEditComponentTestable; + const testable = component as PanelEditComponentTestable; testable.testResults.set([{ name: 'John', count: 10 }]); testable.labelColumn.set('name'); testable.seriesList.set([{ value_column: 'count' }]); diff --git a/frontend/src/app/components/charts/chart-edit/chart-edit.component.ts b/frontend/src/app/components/charts/panel-edit/panel-edit.component.ts similarity index 97% rename from frontend/src/app/components/charts/chart-edit/chart-edit.component.ts rename to frontend/src/app/components/charts/panel-edit/panel-edit.component.ts index fb31c787a..1114b9065 100644 --- a/frontend/src/app/components/charts/chart-edit/chart-edit.component.ts +++ b/frontend/src/app/components/charts/panel-edit/panel-edit.component.ts @@ -17,7 +17,6 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { CodeEditorModule } from '@ngstack/code-editor'; import { Angulartics2 } from 'angulartics2'; import posthog from 'posthog-js'; -import { firstValueFrom } from 'rxjs'; import { DEFAULT_COLOR_PALETTE } from 'src/app/lib/chart-config.helper'; import { ChartAxisConfig, @@ -30,7 +29,6 @@ import { GeneratedPanelWithPosition, TestQueryResult, } from 'src/app/models/saved-query'; -import { TableProperties } from 'src/app/models/table'; import { ConnectionsService } from 'src/app/services/connections.service'; import { DashboardsService } from 'src/app/services/dashboards.service'; import { SavedQueriesService } from 'src/app/services/saved-queries.service'; @@ -41,9 +39,9 @@ import { AlertComponent } from '../../ui-components/alert/alert.component'; import { ChartPreviewComponent } from '../chart-preview/chart-preview.component'; @Component({ - selector: 'app-chart-edit', - templateUrl: './chart-edit.component.html', - styleUrls: ['./chart-edit.component.css'], + selector: 'app-panel-edit', + templateUrl: './panel-edit.component.html', + styleUrls: ['./panel-edit.component.css'], imports: [ CommonModule, FormsModule, @@ -64,7 +62,7 @@ import { ChartPreviewComponent } from '../chart-preview/chart-preview.component' DashboardsSidebarComponent, ], }) -export class ChartEditComponent implements OnInit { +export class PanelEditComponent implements OnInit { protected connectionId = signal(''); protected queryId = signal(''); protected isEditMode = signal(false); @@ -130,10 +128,11 @@ export class ChartEditComponent implements OnInit { protected colorPalette = signal([]); // AI generation + private _tables = inject(TablesService); protected aiDescription = signal(''); protected aiTableName = signal(''); protected aiGenerating = signal(false); - protected aiTables = signal([]); + protected aiTables = this._tables.tablesSignal; protected aiDashboardId = signal(null); protected aiExpanded = signal(true); protected manualExpanded = signal(false); @@ -374,7 +373,6 @@ export class ChartEditComponent implements OnInit { private _savedQueries = inject(SavedQueriesService); private _connections = inject(ConnectionsService); private _dashboards = inject(DashboardsService); - private _tables = inject(TablesService); private _uiSettings = inject(UiSettingsService); private route = inject(ActivatedRoute); private router = inject(Router); @@ -722,20 +720,11 @@ export class ChartEditComponent implements OnInit { } } - private async _loadAiPrerequisites(): Promise { + private _loadAiPrerequisites(): void { const connectionId = this.connectionId(); if (!connectionId) return; - try { - const tables = await firstValueFrom(this._tables.fetchTables(connectionId)); - if (tables?.length) { - this.aiTables.set(tables); - } - } catch { - // Tables loading failed - AI feature will be unavailable - } - - // Trigger dashboards loading; the effect in constructor picks up the result + this._tables.setActiveConnectionForTables(connectionId); this._dashboards.setActiveConnection(connectionId); } } diff --git a/frontend/src/app/services/tables.service.ts b/frontend/src/app/services/tables.service.ts index 2df9e2598..c83aaaa42 100644 --- a/frontend/src/app/services/tables.service.ts +++ b/frontend/src/app/services/tables.service.ts @@ -1,12 +1,13 @@ import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { computed, Injectable, inject, signal } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { Angulartics2 } from 'angulartics2'; import posthog from 'posthog-js'; import { BehaviorSubject, EMPTY, throwError } from 'rxjs'; import { catchError, filter, map } from 'rxjs/operators'; import { AlertActionType, AlertType } from '../models/alert'; -import { PersonalTableViewSettings, Rule, TableSettings, Widget } from '../models/table'; +import { PersonalTableViewSettings, Rule, TableProperties, TableSettings, Widget } from '../models/table'; +import { ApiService } from './api.service'; import { NotificationsService } from './notifications.service'; export enum SortOrdering { @@ -38,6 +39,24 @@ export class TablesService { private tables = new BehaviorSubject(''); public cast = this.tables.asObservable(); + // Signal-based tables fetching + private _api = inject(ApiService); + private _activeConnectionId = signal(null); + private _tablesResource = this._api.resource( + () => { + const connectionId = this._activeConnectionId(); + if (!connectionId) return undefined; + return `/connection/tables/${connectionId}`; + }, + { errorMessage: 'Failed to fetch tables' }, + ); + public readonly tablesSignal = computed(() => this._tablesResource.value() ?? []); + public readonly tablesLoading = computed(() => this._tablesResource.isLoading()); + + setActiveConnectionForTables(connectionId: string): void { + this._activeConnectionId.set(connectionId); + } + constructor( private _http: HttpClient, private router: Router, @@ -80,7 +99,7 @@ export class TablesService { } fetchTablesFolders(connectionID: string, hidden?: boolean) { - console.log('fetchTablesFolders service') + console.log('fetchTablesFolders service'); return this._http .get(`/table-categories/v2/${connectionID}`, { params: { diff --git "a/frontend/src/assets/icons/db-logos/\321\201assandra_logo.svg" b/frontend/src/assets/icons/db-logos/cassandra_logo.svg similarity index 100% rename from "frontend/src/assets/icons/db-logos/\321\201assandra_logo.svg" rename to frontend/src/assets/icons/db-logos/cassandra_logo.svg diff --git "a/frontend/src/assets/icons/db-logos/\321\201assandra_logo_dark.svg" b/frontend/src/assets/icons/db-logos/cassandra_logo_dark.svg similarity index 100% rename from "frontend/src/assets/icons/db-logos/\321\201assandra_logo_dark.svg" rename to frontend/src/assets/icons/db-logos/cassandra_logo_dark.svg