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
2 changes: 1 addition & 1 deletion frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@
</mat-tab-nav-panel>

<div *ngIf="!authBarTheme" class="footer">
<span class="footer__text">&copy; 2025 Rocketadmin • v{{appVersion}}</span>
<span class="footer__text">&copy; 2026 Rocketadmin • v{{appVersion}}</span>
</div>
<app-feature-notification *ngIf="isFeatureNotificationShown" (dismiss)="dismissFeatureNotification()"></app-feature-notification>
</mat-sidenav-content>
Expand Down
12 changes: 4 additions & 8 deletions frontend/src/app/components/audit/audit.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,17 @@ 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();
});

it('should create', () => {
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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ <h3 class='mat-subheading-2'>Rocketadmin can not find any tables</h3>
<app-db-tables-list
[collapsed]="!shownTableTitles"
[connectionTitle]="connectionTitle"
[tables]="tablesList"
[tableFolders]="tableFolders"
[connectionID]="connectionID"
[selectedTable]="selectedTableName"
[uiSettings]="uiSettings"
Expand Down Expand Up @@ -98,8 +98,7 @@ <h3 class='mat-subheading-2'>Rocketadmin can not find any tables</h3>
[connectionID]="connectionID"
[isTestConnection]="currentConnectionIsTest"
[accessLevel]="currentConnectionAccessLevel"
[tables]="tablesList"
[folders]="tableFolders"
[tableFolders]="tableFolders"
(openFilters)="openTableFilters($event)"
(removeFilter)="removeFilter($event)"
(resetAllFilters)="clearAllFilters()"
Expand Down
18 changes: 13 additions & 5 deletions frontend/src/app/components/dashboard/dashboard.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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({
Expand All @@ -90,6 +93,10 @@ describe('DashboardComponent', () => {
provide: TablesService,
useValue: fakeTablesService,
},
{
provide: TableRowService,
useValue: { cast: new BehaviorSubject('') },
},
{
provide: ActivatedRoute,
useValue: {
Expand Down Expand Up @@ -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));
});
});
192 changes: 84 additions & 108 deletions frontend/src/app/components/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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();
}
});
}
});
Comment on lines +162 to +218

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for the fetchTablesFolders API call. The previous async/await implementation had a try-catch block that handled errors by setting isServerError and serverError properties. Without error handling, if the API call fails, the application may be left in an inconsistent state without informing the user. Add error handling using the catchError operator or a second callback in the subscribe method.

Copilot uses AI. Check for mistakes.
}

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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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 = [];
},
});
}
}
Loading
Loading