diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 481c5b81a..ffb82749f 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -244,7 +244,7 @@ diff --git a/frontend/src/app/components/audit/audit.component.spec.ts b/frontend/src/app/components/audit/audit.component.spec.ts index 5104824b4..1a3fb1443 100644 --- a/frontend/src/app/components/audit/audit.component.spec.ts +++ b/frontend/src/app/components/audit/audit.component.spec.ts @@ -90,6 +90,9 @@ describe('AuditComponent', () => { usersService = TestBed.inject(UsersService); dialog = TestBed.inject(MatDialog); + vi.spyOn(tablesService, 'fetchTables').mockReturnValue(of(mockTablesListResponse)); + vi.spyOn(usersService, 'fetchConnectionUsers').mockReturnValue(of(mockUsersList)); + fixture.autoDetectChanges(); }); @@ -97,14 +100,7 @@ describe('AuditComponent', () => { expect(component).toBeTruthy(); }); - it('should fill users and tables lists', async () => { - vi.spyOn(tablesService, 'fetchTables').mockReturnValue(of(mockTablesListResponse)); - vi.spyOn(usersService, 'fetchConnectionUsers').mockReturnValue(of(mockUsersList)); - - component.ngOnInit(); - fixture.detectChanges(); - await fixture.whenStable(); - + it('should fill users and tables lists', () => { expect(component.tablesList).toEqual([ { table: 'customers', diff --git a/frontend/src/app/components/charts/charts-list/charts-list.component.spec.ts b/frontend/src/app/components/charts/charts-list/charts-list.component.spec.ts index a536165cb..5aa4f444d 100644 --- a/frontend/src/app/components/charts/charts-list/charts-list.component.spec.ts +++ b/frontend/src/app/components/charts/charts-list/charts-list.component.spec.ts @@ -1,6 +1,6 @@ import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { Signal, signal, WritableSignal } from '@angular/core'; +import { NO_ERRORS_SCHEMA, Signal, signal, WritableSignal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; @@ -12,6 +12,7 @@ import { of } from 'rxjs'; import { SavedQuery } from 'src/app/models/saved-query'; import { ConnectionsService } from 'src/app/services/connections.service'; import { QueryUpdateEvent, SavedQueriesService } from 'src/app/services/saved-queries.service'; +import { DashboardsSidebarComponent } from '../../dashboards/dashboards-sidebar/dashboards-sidebar.component'; import { ChartDeleteDialogComponent } from '../chart-delete-dialog/chart-delete-dialog.component'; import { ChartsListComponent } from './charts-list.component'; @@ -91,7 +92,12 @@ describe('ChartsListComponent', () => { }, }, ], - }).compileComponents(); + }) + .overrideComponent(ChartsListComponent, { + remove: { imports: [DashboardsSidebarComponent] }, + add: { schemas: [NO_ERRORS_SCHEMA] }, + }) + .compileComponents(); fixture = TestBed.createComponent(ChartsListComponent); component = fixture.componentInstance; diff --git a/frontend/src/app/components/dashboard/dashboard.component.html b/frontend/src/app/components/dashboard/dashboard.component.html index 05765acc6..57606293c 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.html +++ b/frontend/src/app/components/dashboard/dashboard.component.html @@ -70,7 +70,7 @@

Rocketadmin can not find any tables

Rocketadmin can not find any tables [connectionID]="connectionID" [isTestConnection]="currentConnectionIsTest" [accessLevel]="currentConnectionAccessLevel" - [tables]="tablesList" - [folders]="tableFolders" + [tableFolders]="tableFolders" (openFilters)="openTableFilters($event)" (removeFilter)="removeFilter($event)" (resetAllFilters)="clearAllFilters()" diff --git a/frontend/src/app/components/dashboard/dashboard.component.spec.ts b/frontend/src/app/components/dashboard/dashboard.component.spec.ts index 1de8d670b..2200dfb69 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.spec.ts +++ b/frontend/src/app/components/dashboard/dashboard.component.spec.ts @@ -4,9 +4,10 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { ActivatedRoute, convertToParamMap, provideRouter, Router } from '@angular/router'; import { Angulartics2, Angulartics2Module } from 'angulartics2'; -import { of } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; import { AccessLevel } from 'src/app/models/user'; import { ConnectionsService } from 'src/app/services/connections.service'; +import { TableRowService } from 'src/app/services/table-row.service'; import { TablesService } from 'src/app/services/tables.service'; import { DashboardComponent } from './dashboard.component'; @@ -75,6 +76,8 @@ describe('DashboardComponent', () => { fakeTablesService = { fetchTables: vi.fn().mockReturnValue(of(fakeTables)), + fetchTablesFolders: vi.fn().mockReturnValue(of([{ category_id: 'all-tables-kitten', category_name: 'All Tables', tables: fakeTables }])), + cast: new BehaviorSubject(''), }; await TestBed.configureTestingModule({ @@ -90,6 +93,10 @@ describe('DashboardComponent', () => { provide: TablesService, useValue: fakeTablesService, }, + { + provide: TableRowService, + useValue: { cast: new BehaviorSubject('') }, + }, { provide: ActivatedRoute, useValue: { @@ -121,9 +128,10 @@ describe('DashboardComponent', () => { expect(component.currentConnectionAccessLevel).toEqual('readonly'); }); - it('should call getTables', async () => { - fakeTablesService.fetchTables.mockReturnValue(of(fakeTables)); - const tables = await component.getTables(); - expect(tables).toEqual(fakeTables); + it('should call getData and populate tables', () => { + fakeTablesService.fetchTablesFolders.mockReturnValue(of([{ category_id: 'all-tables-kitten', category_name: 'All Tables', tables: fakeTables }])); + component.getData(); + expect(component.allTables.length).toEqual(fakeTables.length); + expect(component.allTables.map(t => t.table)).toEqual(fakeTables.map(t => t.table)); }); }); diff --git a/frontend/src/app/components/dashboard/dashboard.component.ts b/frontend/src/app/components/dashboard/dashboard.component.ts index 3d3572b42..9194f9868 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.ts +++ b/frontend/src/app/components/dashboard/dashboard.component.ts @@ -1,6 +1,5 @@ import { SelectionModel } from '@angular/cdk/collections'; import { CommonModule } from '@angular/common'; -import { HttpErrorResponse } from '@angular/common/http'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; @@ -12,10 +11,9 @@ import JsonURL from '@jsonurl/jsonurl'; import { Angulartics2, Angulartics2Module } from 'angulartics2'; import { omitBy } from 'lodash-es'; import posthog from 'posthog-js'; -import { first, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { getComparatorsFromUrl } from 'src/app/lib/parse-filter-params'; import { ServerError } from 'src/app/models/alert'; -import { TableCategory } from 'src/app/models/connection'; import { CustomEvent, TableProperties } from 'src/app/models/table'; import { ConnectionSettingsUI, UiSettings } from 'src/app/models/ui-settings'; import { User } from 'src/app/models/user'; @@ -36,7 +34,7 @@ import { BbBulkActionConfirmationDialogComponent } from './db-table-view/db-bulk import { DbTableAiPanelComponent } from './db-table-view/db-table-ai-panel/db-table-ai-panel.component'; import { DbTableFiltersDialogComponent } from './db-table-view/db-table-filters-dialog/db-table-filters-dialog.component'; import { DbTableRowViewComponent } from './db-table-view/db-table-row-view/db-table-row-view.component'; -import { DbTableViewComponent, Folder } from './db-table-view/db-table-view.component'; +import { DbTableViewComponent } from './db-table-view/db-table-view.component'; import { TablesDataSource } from './db-tables-data-source'; import { DbTablesListComponent } from './db-tables-list/db-tables-list.component'; @@ -72,7 +70,9 @@ export class DashboardComponent implements OnInit, OnDestroy { protected posthog = posthog; public isSaas = (environment as any).saas; public user: User = null; - public tablesList: TableProperties[] = null; + get allTables(): TableProperties[] { + return this.tableFolders?.find((cat: any) => cat.category_id === 'all-tables-kitten')?.tables || []; + } public selectedTableName: string; public selectedTableDisplayName: string; public currentPage: number = 1; @@ -98,7 +98,7 @@ export class DashboardComponent implements OnInit, OnDestroy { public isAIpanelOpened: boolean = false; public uiSettings: ConnectionSettingsUI; - public tableFolders: Folder[] = []; + public tableFolders: any[] = []; constructor( private _connections: ConnectionsService, @@ -149,103 +149,99 @@ export class DashboardComponent implements OnInit, OnDestroy { console.log('getData from ngOnInit'); }); - this.loadTableFolders(); + // this.loadTableFolders(); } ngOnDestroy() { this._tableState.clearSelection(); } - async getData() { + getData() { console.log('getData'); - let tables; - try { - tables = await this.getTables(); - } catch (err) { - this.loading = false; - this.isServerError = true; - this.title.setTitle(`Dashboard | ${this._company.companyTabTitle || 'Rocketadmin'}`); - - if (err instanceof HttpErrorResponse) { - this.serverError = { abstract: err.error?.message || err.message, details: err.error?.originalMessage }; - } else { - throw err; - } - } - if (tables && tables.length === 0) { - this.noTablesError = true; - this.loading = false; - this.title.setTitle(`No tables | ${this._company.companyTabTitle || 'Rocketadmin'}`); - } else if (tables) { - this.formatTableNames(tables); - this.route.paramMap - .pipe( - map((params: ParamMap) => { - let tableName = params.get('table-name'); - if (tableName) { - this.selectedTableName = tableName; - this.setTable(tableName); - console.log('setTable from getData paramMap'); - this.title.setTitle( - `${this.selectedTableDisplayName} table | ${this._company.companyTabTitle || 'Rocketadmin'}`, - ); - this.selection.clear(); - } else { - if (this.defaultTableToOpen) { - tableName = this.defaultTableToOpen; + this._tables.fetchTablesFolders(this.connectionID).subscribe((res) => { + console.log('getTables folders') + console.log(res); + + const tables = res.find((item) => item.category_id === 'all-tables-kitten')?.tables || []; + + this.tableFolders = res; + + if (tables && tables.length === 0) { + this.noTablesError = true; + this.loading = false; + this.title.setTitle(`No tables | ${this._company.companyTabTitle || 'Rocketadmin'}`); + } else if (tables) { + this.formatTableNames(); + this.route.paramMap + .pipe( + map((params: ParamMap) => { + let tableName = params.get('table-name'); + if (tableName) { + this.selectedTableName = tableName; + this.setTable(tableName); + console.log('setTable from getData paramMap'); + this.title.setTitle( + `${this.selectedTableDisplayName} table | ${this._company.companyTabTitle || 'Rocketadmin'}`, + ); + this.selection.clear(); } else { - tableName = this.tablesList[0].table; + if (this.defaultTableToOpen) { + tableName = this.defaultTableToOpen; + } else { + tableName = this.allTables[0]?.table; + } + this.router.navigate([`/dashboard/${this.connectionID}/${tableName}`], { replaceUrl: true }); + this.selectedTableName = tableName; } - this.router.navigate([`/dashboard/${this.connectionID}/${tableName}`], { replaceUrl: true }); - this.selectedTableName = tableName; - } - }), - ) - .subscribe(); - this._tableRow.cast.subscribe((arg) => { - if (arg === 'delete row' && this.selectedTableName) { - this.setTable(this.selectedTableName); - console.log('setTable from getData _tableRow cast'); - this.selection.clear(); - } - }); - this._tables.cast.subscribe((arg) => { - if ((arg === 'delete rows' || arg === 'import') && this.selectedTableName) { - this.setTable(this.selectedTableName); - console.log('setTable from getData _tables cast'); - this.selection.clear(); - } - if (arg === 'activate actions') { - this.selection.clear(); - } - }); - } + }), + ) + .subscribe(); + this._tableRow.cast.subscribe((arg) => { + if (arg === 'delete row' && this.selectedTableName) { + this.setTable(this.selectedTableName); + console.log('setTable from getData _tableRow cast'); + this.selection.clear(); + } + }); + this._tables.cast.subscribe((arg) => { + if ((arg === 'delete rows' || arg === 'import') && this.selectedTableName) { + this.setTable(this.selectedTableName); + console.log('setTable from getData _tables cast'); + this.selection.clear(); + } + if (arg === 'activate actions') { + this.selection.clear(); + } + }); + } + }); } - getTables() { - console.log('getTables'); - return this._tables.fetchTables(this.connectionID).toPromise(); + formatTableNames() { + // Format table names inside tableFolders so all components receive formatted tables + this.tableFolders = this.tableFolders.map(category => ({ + ...category, + tables: category.tables.map((tableItem: TableProperties) => this.formatTable(tableItem)), + })); } - formatTableNames(tables: TableProperties[]) { - this.tablesList = tables.map((tableItem: TableProperties) => { - let normalizedTableName; - if (tableItem.display_name) { - normalizedTableName = tableItem.display_name; - } else { - normalizedTableName = normalizeTableName(tableItem.table); + private formatTable(tableItem: TableProperties): TableProperties { + let normalizedTableName; + if (tableItem.display_name) { + normalizedTableName = tableItem.display_name; + } else { + normalizedTableName = normalizeTableName(tableItem.table); + } + const words = normalizedTableName.split(' '); + const initials = words.reduce((result, word) => { + if (word.length > 0) { + return result + word[0].toUpperCase(); } - const words = normalizedTableName.split(' '); - const initials = words.reduce((result, word) => { - if (word.length > 0) { - return result + word[0].toUpperCase(); - } - return result; - }, ''); + return result; + }, ''); - return { ...tableItem, normalizedTableName, initials: initials.slice(0, 2) }; - }); + return { ...tableItem, normalizedTableName, initials: initials.slice(0, 2) }; } setTable(tableName: string) { @@ -262,7 +258,7 @@ export class DashboardComponent implements OnInit, OnDestroy { this.getRows(search); console.log('getRows from setTable'); - const selectedTableProperties = this.tablesList.find((table: any) => table.table === this.selectedTableName); + const selectedTableProperties = this.allTables.find((table: any) => table.table === this.selectedTableName); if (selectedTableProperties) { this.selectedTableDisplayName = selectedTableProperties.display_name || normalizeTableName(selectedTableProperties.table); @@ -437,24 +433,4 @@ export class DashboardComponent implements OnInit, OnDestroy { this.shownTableTitles = !this.shownTableTitles; this._uiSettings.updateConnectionSetting(this.connectionID, 'shownTableTitles', this.shownTableTitles); } - - private loadTableFolders() { - this._connections.getTablesFolders(this.connectionID).subscribe({ - next: (categories: TableCategory[]) => { - if (categories && categories.length > 0) { - this.tableFolders = categories.map((cat) => ({ - id: cat.category_id, - name: cat.category_name, - tableIds: cat.tables, - })); - } else { - this.tableFolders = []; - } - }, - error: (error) => { - console.error('Error fetching table folders:', error); - this.tableFolders = []; - }, - }); - } } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index f0210b7d2..299cb757b 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -5,23 +5,18 @@

{{ displayName }}

Table - - + + - {{table.normalizedTableName}} - - - - - {{table.normalizedTableName}} + {{table.normalizedTableName || table.display_name || table.table}} - - - {{table.normalizedTableName}} + + + {{table.normalizedTableName || table.display_name || table.table}} - + diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index d1c292331..4a81fc688 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -69,10 +69,11 @@ interface Column { selected: boolean; } -export interface Folder { - id: string; - name: string; - tableIds: string[]; +interface TableCategory { + category_id: string; + category_name: string; + category_color?: string; + tables: TableProperties[]; } @Component({ @@ -121,8 +122,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { @Input() activeFilters: object; @Input() filterComparators: object; @Input() selection: SelectionModel; - @Input() tables: TableProperties[]; - @Input() folders: Folder[] = []; + @Input() tableFolders: TableCategory[] = []; @Output() openFilters = new EventEmitter(); @Output() openPage = new EventEmitter(); @@ -139,7 +139,6 @@ export class DbTableViewComponent implements OnInit, OnChanges { // public tablesSwitchControl = new FormControl(''); public tableData: any; - public filteredTables: TableProperties[]; // public selection: any; public columns: Column[]; public displayedColumns: string[] = []; @@ -293,19 +292,24 @@ export class DbTableViewComponent implements OnInit, OnChanges { }); } - onInput(searchValue: string) { - this.filteredTables = this.filterTables(searchValue); + get allTables(): TableProperties[] { + return this.tableFolders?.find((cat) => cat.category_id === null)?.tables || []; } - onInputFocus() { - this.filteredTables = this.tables; + get tableFoldersForSelect(): TableCategory[] { + if (!this.tableFolders) return []; + return this.tableFolders.filter((cat) => cat.category_id !== null); } - private filterTables(searchValue: string): any[] { - const filterValue = searchValue.toLowerCase(); - return this.tables.filter((table) => table.normalizedTableName.toLowerCase().includes(filterValue)); + get uncategorizedTables(): TableProperties[] { + const tablesInFolders = new Set(); + this.tableFoldersForSelect.forEach((folder) => { + (folder.tables || []).forEach((t) => tablesInFolders.add(t.table)); + }); + return this.allTables.filter((t) => !tablesInFolders.has(t.table)); } + loadRowsPage() { this.tableRelatedRecords = null; this.tableData.fetchRows({ @@ -321,13 +325,6 @@ export class DbTableViewComponent implements OnInit, OnChanges { }); } - // ngOnChanges(changes: SimpleChanges) { - // if (changes.name?.currentValue && this.paginator) { - // this.paginator.pageIndex = 0; - // this.searchString = ''; - // } - // } - isSortable(column: string) { return this.tableData.sortByColumns.includes(column) || !this.tableData.sortByColumns.length; } @@ -692,17 +689,10 @@ export class DbTableViewComponent implements OnInit, OnChanges { } } - getFolderTables(folder: Folder): TableProperties[] { - return this.tables.filter((table) => folder.tableIds.includes(table.table)); + getFolderTables(folder: TableCategory): TableProperties[] { + return folder.tables || []; } - getUncategorizedTables(): TableProperties[] { - const categorizedTableIds = new Set(); - this.folders.forEach((folder) => { - folder.tableIds.forEach((id) => categorizedTableIds.add(id)); - }); - return this.tables.filter((table) => !categorizedTableIds.has(table.table)); - } onFilterSelected($event) { console.log('table view fiers filterSelected:', $event); 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 6a55c9269..56225fb98 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 @@ -689,26 +689,13 @@ } .more-folder-button { - width: 16px; - height: 16px; - line-height: 16px; - border: none; - border-radius: 0; - background-color: transparent !important; - color: #bdbdbd !important; - margin-left: 4px; -} - -.more-folder-button:hover { - border: none !important; - background-color: rgba(0, 0, 0, 0.02) !important; - color: #9e9e9e !important; + line-height: 18px; } .more-folder-button mat-icon { - font-size: 12px; - width: 12px; - height: 12px; + font-size: 18px; + width: 18px; + height: 18px; } .folder-name { @@ -1181,3 +1168,55 @@ padding: 16px 20px; border-top: 1px solid #e0e0e0; } + +/* CDK Drag-Drop reordering styles */ +.table-drag-handle { + color: #bdbdbd; + cursor: grab; + font-size: 16px; + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 2px; + transition: color 0.2s ease; +} + +.selected-table-item:hover .table-drag-handle { + color: #757575; +} + +.cdk-drag-preview.selected-table-item { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); + background: #ffffff; + opacity: 0.9; +} + +.cdk-drag-placeholder { + opacity: 0.3; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.selected-tables-list.cdk-drop-list-dragging .selected-table-item:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +@media (prefers-color-scheme: dark) { + .table-drag-handle { + color: #616161; + } + + .selected-table-item:hover .table-drag-handle { + color: #9e9e9e; + } + + .cdk-drag-preview.selected-table-item { + background: #424242; + } +} diff --git a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html index f3b4b6a1c..161541582 100644 --- a/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html +++ b/frontend/src/app/components/dashboard/db-tables-list/db-tables-list.component.html @@ -1,8 +1,8 @@ - + -
+
@@ -24,9 +24,9 @@ [matTooltip]="folder.name" matTooltipPosition="right" (click)="onCollapsedFolderClick(folder)"> - dehaze - folder
@@ -95,7 +95,7 @@
@@ -103,10 +103,10 @@ [class.expanded]="folder.expanded" (click)="toggleFolder(folder.id)"> - dehaze - folder @@ -115,7 +115,7 @@
-