Skip to content
Open
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
5 changes: 5 additions & 0 deletions frontend/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
"glob": "**/*",
"input": "./node_modules/monaco-editor/min",
"output": "./assets/monaco"
},
{
"glob": "cedar_wasm_bg.wasm",
"input": "./node_modules/@cedar-policy/cedar-wasm/web",
"output": "./assets/cedar-wasm"
}
],
"styles": [
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@angular/platform-browser-dynamic": "~20.3.16",
"@angular/router": "~20.3.16",
"@brumeilde/ngx-theme": "^1.2.1",
"@cedar-policy/cedar-wasm": "^4.9.1",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/noto-sans": "^5.2.10",
"@jsonurl/jsonurl": "^1.1.8",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ <h1 class="mat-h1 connectForm__fullLine">
</div>

<!-- edit connection actions -->
<div class="actions" *ngIf="accessLevel && db.id && accessLevel === 'edit' && !db.isTestConnection">
<div class="actions" *ngIf="canEditConnection() && db.id && !db.isTestConnection">
<button type="button" mat-button color="warn"
class="delete-button"
data-testid="edit-connection-actions-delete-button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe('ConnectDBComponent', () => {
updateConnection: vi.fn(),
getCurrentConnectionTitle: vi.fn(),
currentConnectionID: '9d5f6d0f-9516-4598-91c4-e4fe6330b4d4',
canEditConnection: vi.fn().mockReturnValue(true),
};

const connectionCredsApp = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export class ConnectDBComponent implements OnInit {
return this._connections.currentConnection;
}

protected canEditConnection = () => this._connections.canEditConnection();

get accessLevel(): AccessLevel {
return this._connections.currentConnectionAccessLevel;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ <h3 class='mat-subheading-2'>Rocketadmin can not find any tables</h3>
<p class="mat-body-1">{{serverError.abstract}}</p>
</ng-template>
<div class="error-actions">
<a mat-stroked-button *ngIf="accessLevel === 'edit'"
<a mat-stroked-button *ngIf="canEditConnection()"
routerLink="/edit-db/{{connectionID}}">
Check database credentials
</a>
<button *ngIf="isSaas" mat-flat-button color="warn" (click)="openIntercome()">Chat with support</button>
<button type="button" *ngIf="isSaas" mat-flat-button color="warn" (click)="openIntercome()">Chat with support</button>
<a *ngIf="!isSaas" mat-flat-button color="warn"
href="https://github.com/rocket-admin/rocketadmin/issues" target="_blank">
Report a bug
Expand Down Expand Up @@ -147,7 +147,7 @@ <h3 class='mat-subheading-2'>Rocketadmin can not find any tables</h3>
Log changes in tables
</mat-slide-toggle>
</div>
<div *ngIf="accessLevel !== 'readonly'" class="actions">
<div *ngIf="canEditConnection()" class="actions">
<button mat-flat-button color="warn"
type="button"
[disabled]="!isSettingsExist || submitting || connectionSettingsForm.form.invalid"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { take } from 'rxjs';
import { ServerError } from 'src/app/models/alert';
import { ConnectionSettings } from 'src/app/models/connection';
import { TableProperties } from 'src/app/models/table';
import { AccessLevel } from 'src/app/models/user';
import { CompanyService } from 'src/app/services/company.service';
import { ConnectionsService } from 'src/app/services/connections.service';
import { TablesService } from 'src/app/services/tables.service';
Expand Down Expand Up @@ -113,9 +112,7 @@ export class ConnectionSettingsComponent implements OnInit {
return this._connections.currentConnection.title || this._connections.currentConnection.database;
}

get accessLevel(): AccessLevel {
return this._connections.currentConnectionAccessLevel;
}
protected canEditConnection = () => this._connections.canEditConnection();

getSettings() {
this._connections.getConnectionSettings(this.connectionID).subscribe((res: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
<p class="mat-body-1">{{serverError.abstract}}</p>
</ng-template>
<div class="error-actions">
<a mat-stroked-button *ngIf="currentConnectionAccessLevel === 'edit'"
<a mat-stroked-button *ngIf="canEditConnection()"
routerLink="/edit-db/{{connectionID}}">
Check database credentials
</a>
<button *ngIf="isSaas" mat-flat-button color="warn" (click)="openIntercome()">Chat with support</button>
<button type="button" *ngIf="isSaas" mat-flat-button color="warn" (click)="openIntercome()">Chat with support</button>
<a *ngIf="!isSaas" mat-flat-button color="warn"
href="https://github.com/rocket-admin/rocketadmin/issues" target="_blank">
Report a bug
Expand Down Expand Up @@ -89,15 +89,14 @@ <h3 class='mat-subheading-2'>Rocketadmin can not find any tables</h3>
[connectionID]="connectionID"
[selectedTable]="selectedTableName"
[uiSettings]="uiSettings"
[accessLevel]="currentConnectionAccessLevel"
(expandSidebar)="toggleSideBar()">
</app-db-tables-list>
</mat-sidenav>
<mat-sidenav-content class="table-preview">
<div class="table-preview-content">
<div class="alerts">
<app-alert></app-alert>
<div *ngIf="dataSource.alert_settingsInfo && !isConfiguring" class="ai-config-alert">
<div *ngIf="dataSource.alert_settingsInfo && canEditConnection() && !isConfiguring" class="ai-config-alert">
<mat-icon class="ai-config-alert__icon">auto_awesome</mat-icon>
<div class="ai-config-alert__message mat-body-1">
<strong class="ai-config-alert__title">New: AI Configuration</strong>
Expand All @@ -122,7 +121,6 @@ <h3 class='mat-subheading-2'>Rocketadmin can not find any tables</h3>
[selection]="selection"
[connectionID]="connectionID"
[isTestConnection]="currentConnectionIsTest"
[accessLevel]="currentConnectionAccessLevel"
[tableFolders]="tableFolders"
(openFilters)="openTableFilters($event)"
(removeFilter)="removeFilter($event)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('DashboardComponent', () => {
return AccessLevel.None;
},
getTablesFolders: () => of([]),
canEditConnection: () => false,
};
const fakeRouter = {
navigate: vi.fn().mockReturnValue(Promise.resolve('')),
Expand Down
120 changes: 62 additions & 58 deletions frontend/src/app/components/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
private angulartics2: Angulartics2,
) {}

protected canEditConnection = () => this._connections.canEditConnection();

get currentConnectionAccessLevel() {
return this._connections.currentConnectionAccessLevel;
}
Expand Down Expand Up @@ -166,66 +168,68 @@ export class DashboardComponent implements OnInit, OnDestroy {
getData() {
console.log('getData');

this._tables.fetchTablesFolders(this.connectionID).subscribe((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 {
if (this.defaultTableToOpen) {
tableName = this.defaultTableToOpen;
this._tables.fetchTablesFolders(this.connectionID).subscribe(
(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.allTables[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();
}
});
}
},
(err) => {
this.isServerError = true;
this.serverError = { abstract: err.error?.message || err.message, details: err.error?.originalMessage };
this.loading = false;
this.title.setTitle(`Error | ${this._company.companyTabTitle || 'Rocketadmin'}`);
});
}),
)
.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 +183 to +223
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getData() is accumulating subscriptions on every refresh.

Each successful fetch adds fresh subscriptions to route.paramMap, TableRowService.cast, and TablesService.cast. Because getData() is called again from the UI settings stream, those handlers start firing multiple times and none of them are torn down on destroy.

Based on learnings, frontend/**/*.component.ts: Use takeUntil pattern for memory leak prevention with proper subscription management.

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

In `@frontend/src/app/components/dashboard/dashboard.component.ts` around lines
184 - 224, getData() is re-subscribing to route.paramMap, this._tableRow.cast
and this._tables.cast each time it's called, leaking handlers; fix by
introducing a component-level destroy notifier (e.g., private destroy$ = new
Subject<void>()), pipe each subscription through takeUntil(this.destroy$) where
you subscribe to route.paramMap, this._tableRow.cast and this._tables.cast
inside getData(), and implement ngOnDestroy to call this.destroy$.next() and
this.destroy$.complete(); alternatively ensure those subscriptions are created
once (outside repeated getData() calls) and still use takeUntil to guarantee
teardown.

}
},
(err) => {
this.isServerError = true;
this.serverError = { abstract: err.error?.message || err.message, details: err.error?.originalMessage };
this.loading = false;
this.title.setTitle(`Error | ${this._company.companyTabTitle || 'Rocketadmin'}`);
},
);
}

formatTableNames() {
Expand Down
Loading
Loading