Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
97a90d6
refactor: migrate cedar policy and group management to Angular signals
gugu Mar 25, 2026
cba862f
feat: add cedar-wasm policy validation and permission service
gugu Mar 25, 2026
3a692f0
refactor: replace old permission checks with CedarPermissionService.canI
gugu Mar 25, 2026
7948498
refactor: replace remaining permission checks with CedarPermissionSer…
gugu Mar 25, 2026
54b9a6e
refactor: simplify permission handling by centralizing canEditConnect…
gugu Mar 26, 2026
9069b24
fix: refresh groups directly in saveCedarPolicy to ensure permissions…
gugu Mar 26, 2026
d012a4c
fix: dashboard:create action should not require a resource parameter
gugu Mar 26, 2026
9993894
permissions: fix tracking field of member object
lyubov-voloshko Mar 27, 2026
8482771
fix: add-user-to-group button was not submitting the form
gugu Mar 27, 2026
e652dd1
merge: resolve conflict keeping both canEditConnection and !isConfigu…
gugu Mar 27, 2026
4b5dba4
partial permissions support
gugu Mar 28, 2026
8c4397c
extract demo-login into separate component, improve hostname validator
gugu Apr 3, 2026
5de6a06
Merge branch 'main' into migrate-cedar-policy-groups-to-signals
gugu Apr 6, 2026
2e79f6e
permissions: add View as group + make merged Cedar policies resilient
gugu Apr 7, 2026
d7c4ab7
Merge branch 'main' into migrate-cedar-policy-groups-to-signals
gugu Apr 7, 2026
b76ba10
fix: avoid < in own-connections @if to satisfy Angular and Biome
gugu Apr 7, 2026
15c7ffb
fix: wrap empty-password early return in Promise.resolve
gugu Apr 8, 2026
ac0960a
Merge branch 'main' into migrate-cedar-policy-groups-to-signals
gugu Apr 8, 2026
759336d
Merge branch 'main' into migrate-cedar-policy-groups-to-signals
gugu Apr 8, 2026
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 @@ -74,15 +74,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" class="ai-config-alert">
<div *ngIf="dataSource.alert_settingsInfo && canEditConnection()" 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 @@ -106,7 +105,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 @@ -114,6 +114,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 @@ -159,66 +161,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
Copy Markdown

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