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
4 changes: 2 additions & 2 deletions frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,13 @@
class="action">
Login
</a>
<a mat-flat-button color="accent" routerLink="/registration"
<a *ngIf="isSaas" mat-flat-button color="accent" routerLink="/registration"
data-testid="registration-header-link"
class="action">
Sign up
</a>
</ng-container>
<ng-container *ngIf="page === '/registration'">
<ng-container *ngIf="page === '/registration' && isSaas">
<a mat-flat-button color="accent" routerLink="/login"
data-testid="login-header-link"
class="action">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/components/company/company.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ <h3>Configure DNS Records</h3>
</div>
</ng-container >

<div *ngIf="currentUser && currentUser.role === 'USER' && currentPlan !== 'free' && companyCustomDomainHostname">
<div *ngIf="isSaas && currentUser && currentUser.role === 'USER' && currentPlan !== 'free' && companyCustomDomainHostname">
<strong>Your admin panel address: </strong>
<span data-testid="company-custom-domain">{{companyCustomDomainHostname}}</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ <h3 class='mat-subheading-2'>Rocketadmin can not find any tables</h3>
[isTestConnection]="currentConnectionIsTest"
[accessLevel]="currentConnectionAccessLevel"
[tables]="tablesList"
[folders]="tableFolders"
(openFilters)="openTableFilters($event)"
(removeFilter)="removeFilter($event)"
(resetAllFilters)="clearAllFilters()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ describe('DashboardComponent', () => {
},
get currentConnectionAccessLevel(): AccessLevel {
return AccessLevel.None;
}
},
getTablesFolders: () => of([])
};
const fakeRouter = jasmine.createSpyObj('Router', {navigate: Promise.resolve('')});

Expand Down
27 changes: 26 additions & 1 deletion frontend/src/app/components/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { Angulartics2, Angulartics2Module } from 'angulartics2';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ConnectionSettingsUI, UiSettings } from 'src/app/models/ui-settings';
import { CustomEvent, TableProperties } from 'src/app/models/table';
import { map } from 'rxjs/operators';
import { TableCategory } from 'src/app/models/connection';
import { Folder } from './db-table-view/db-table-view.component';
import { first, map } from 'rxjs/operators';

import { AlertComponent } from '../ui-components/alert/alert.component';
import { BannerComponent } from '../ui-components/banner/banner.component';
Expand Down Expand Up @@ -101,6 +103,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
public isAIpanelOpened: boolean = false;

public uiSettings: ConnectionSettingsUI;
public tableFolders: Folder[] = [];
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The property name tableFolders could be more consistent with the API terminology. Since the backend API endpoint is /table-categories and uses the TableCategory interface, consider renaming to tableCategories to maintain consistency with the domain model throughout the codebase.

Copilot uses AI. Check for mistakes.

constructor(
private _connections: ConnectionsService,
Expand Down Expand Up @@ -153,6 +156,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.getData();
console.log('getData from ngOnInit');
});

this.loadTableFolders();
}

ngOnDestroy() {
Expand Down Expand Up @@ -429,4 +434,24 @@ 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 = [];
}
});
}
Comment on lines +438 to +456
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The new loadTableFolders method lacks test coverage. Since this component has a spec file with existing tests, please add tests to verify this method correctly handles successful API responses, error scenarios, and empty category arrays.

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@ <h2 class="mat-h2 table-name">{{ displayName }}</h2>

<mat-form-field appearance="outline" class="table-switcher">
<mat-label>Table</mat-label>
<input type="text"
placeholder="Select a table"
matInput
[value]="name"
[matAutocomplete]="auto"
(focus)="onInputFocus()"
(input)="onInput($event.target.value)">
<mat-autocomplete autoActiveFirstOption
#auto="matAutocomplete">
<mat-option *ngFor="let table of filteredTables"
class="table-switcher-option"
[value]="table.table">
<a class="table-switcher-link"
routerLink="/dashboard/{{connectionID}}/{{table.table}}"
[queryParams]="{page_index: 0, page_size: 30}">
<mat-select [value]="name" (selectionChange)="switchTable($event.value)">
<ng-container *ngIf="folders && folders.length > 0; else flatList">
<mat-optgroup *ngFor="let folder of folders" [label]="folder.name">
<mat-option *ngFor="let table of getFolderTables(folder)" [value]="table.table">
{{table.normalizedTableName}}
</mat-option>
</mat-optgroup>
<mat-optgroup *ngIf="getUncategorizedTables().length > 0" label="Other">
<mat-option *ngFor="let table of getUncategorizedTables()" [value]="table.table">
{{table.normalizedTableName}}
</mat-option>
</mat-optgroup>
</ng-container>
<ng-template #flatList>
<mat-option *ngFor="let table of tables" [value]="table.table">
{{table.normalizedTableName}}
</a>
</mat-option>
</mat-autocomplete>
</mat-option>
</ng-template>
</mat-select>
</mat-form-field>

<button mat-icon-button (click)="loadRowsPage()">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { DynamicModule } from 'ng-dynamic-component';
import { ForeignKeyDisplayComponent } from '../../ui-components/table-display-fields/foreign-key/foreign-key.component';
import JsonURL from "@jsonurl/jsonurl";
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatSelectModule } from '@angular/material/select';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
Expand Down Expand Up @@ -51,6 +52,12 @@ interface Column {
selected: boolean
}

export interface Folder {
id: string;
name: string;
tableIds: string[];
}
Comment on lines +55 to +59
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The Folder interface is exported from this component file but is primarily a data model. Consider moving it to a dedicated models file (e.g., src/app/models/folder.ts or alongside TableCategory in src/app/models/connection.ts) to improve code organization and reusability, especially since it's imported by the dashboard component.

Copilot uses AI. Check for mistakes.

@Component({
selector: 'app-db-table-view',
templateUrl: './db-table-view.component.html',
Expand All @@ -71,6 +78,7 @@ interface Column {
ReactiveFormsModule,
MatInputModule,
MatAutocompleteModule,
MatSelectModule,
MatMenuModule,
MatTooltipModule,
ClipboardModule,
Expand All @@ -95,6 +103,7 @@ export class DbTableViewComponent implements OnInit {
@Input() filterComparators: object;
@Input() selection: SelectionModel<any>;
@Input() tables: TableProperties[];
@Input() folders: Folder[] = [];

@Output() openFilters = new EventEmitter();
@Output() openPage = new EventEmitter();
Expand Down Expand Up @@ -514,8 +523,24 @@ export class DbTableViewComponent implements OnInit {
this._notifications.showSuccessSnackbar(message);
}

switchTable(_e) {
switchTable(tableName: string) {
if (tableName && tableName !== this.name) {
this.router.navigate([`/dashboard/${this.connectionID}/${tableName}`], {
queryParams: { page_index: 0, page_size: 30 }
});
}
}

getFolderTables(folder: Folder): TableProperties[] {
return this.tables.filter(table => folder.tableIds.includes(table.table));
}

getUncategorizedTables(): TableProperties[] {
const categorizedTableIds = new Set<string>();
this.folders.forEach(folder => {
folder.tableIds.forEach(id => categorizedTableIds.add(id));
});
return this.tables.filter(table => !categorizedTableIds.has(table.table));
}
Comment on lines +534 to 544
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The getFolderTables and getUncategorizedTables methods should handle the case where this.tables could be undefined or null. Consider adding null checks to prevent potential runtime errors when these properties are not yet initialized.

Copilot uses AI. Check for mistakes.
Comment on lines +526 to 544
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The new methods switchTable, getFolderTables, and getUncategorizedTables lack test coverage. Since the component has a spec file with existing tests, please add tests for these new methods to verify they work correctly with folders and handle edge cases like empty folder arrays or uncategorized tables.

Copilot uses AI. Check for mistakes.

onFilterSelected($event) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@
transform: translateX(-10%) scale(0.8);
}

@media (width <= 600px) {
.filters-container {
flex-direction: column;
transform: none;
}
}

.dynamic-column-editor {
display: flex;
align-items: center;
Expand All @@ -83,6 +90,20 @@
margin-top: -8px;
}

.comparator-text {
display: none;
}

@media (width <= 600px) {
.comparator-select {
display: none;
}

.comparator-text {
display: inline;
}
}

@media (prefers-color-scheme: light) {
.column-name {
color: rgba(0,0,0,0.64);
Expand Down Expand Up @@ -118,6 +139,12 @@
padding-bottom: 16px;
}

@media (width <= 600px) {
.static-filters {
transform: none;
}
}

.static-filter-chip {
cursor: default;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,14 @@
<strong *ngIf="getComparatorType(getInputType(savedFilterMap[selectedFilterSetId]?.dynamicColumn.column)) !== 'nonComparable'">
{{ savedFilterMap[selectedFilterSetId]?.dynamicColumn.column }}
</strong>
<span *ngIf="getComparatorType(getInputType(savedFilterMap[selectedFilterSetId]?.dynamicColumn.column)) === 'text'" class="comparator-text">
{{ {startswith: 'starts with', endswith: 'ends with', eq: 'equal', contains: 'contains', icontains: 'not contains', empty: 'is empty'}[savedFilterMap[selectedFilterSetId].dynamicColumn.operator] }}
</span>
<span *ngIf="getComparatorType(getInputType(savedFilterMap[selectedFilterSetId]?.dynamicColumn.column)) === 'number'" class="comparator-text">
{{ {eq: '=', gt: '>', lt: '<', gte: '≥', lte: '≤'}[savedFilterMap[selectedFilterSetId].dynamicColumn.operator] }}
</span>
</span>
<mat-form-field *ngIf="getComparatorType(getInputType(savedFilterMap[selectedFilterSetId]?.dynamicColumn.column)) === 'text'" appearance="outline">
<mat-form-field *ngIf="getComparatorType(getInputType(savedFilterMap[selectedFilterSetId]?.dynamicColumn.column)) === 'text'" appearance="outline" class="comparator-select">
<mat-select [(ngModel)]="savedFilterMap[selectedFilterSetId].dynamicColumn.operator"
name="textComparator"
(ngModelChange)="updateDynamicColumnComparator($event)">
Expand All @@ -62,7 +68,7 @@
</mat-select>
</mat-form-field>

<mat-form-field *ngIf="getComparatorType(getInputType(savedFilterMap[selectedFilterSetId]?.dynamicColumn.column)) === 'number'" appearance="outline">
<mat-form-field *ngIf="getComparatorType(getInputType(savedFilterMap[selectedFilterSetId]?.dynamicColumn.column)) === 'number'" appearance="outline" class="comparator-select">
<mat-select [(ngModel)]="savedFilterMap[selectedFilterSetId].dynamicColumn.operator"
name="numberComparator"
(ngModelChange)="updateDynamicColumnComparator($event)">
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/app/components/login/login.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<div class="login-page">
<form
[ngClass]="{
'login-form': !isCustomDomain,
'login-form--native-login': isCustomDomain
'login-form': isSaas && !isCustomDomain,
'login-form--native-login': !isSaas || isCustomDomain
}"
[hidden]="is2FAShown"
#loginForm="ngForm"
Expand Down Expand Up @@ -65,9 +65,9 @@ <h1 class="mat-headline-4 loginTitle">
<!-- <mat-error *ngIf="port.errors?.required && (port.invalid && port.touched)">Port should not be empty.</mat-error> -->
</mat-form-field>

<div *ngIf="!isCustomDomain" class="divider"><span class="divider__label">or</span></div>
<div *ngIf="isSaas && !isCustomDomain" class="divider"><span class="divider__label">or</span></div>

<div id="google_login_button" *ngIf="!isCustomDomain"
<div id="google_login_button" *ngIf="isSaas && !isCustomDomain"
class="login-form__google-button"
angulartics2On="click"
angularticsAction="Login: login with google is clicked">
Expand All @@ -82,7 +82,7 @@ <h1 class="mat-headline-4 loginTitle">
<span class="login-form__github-caption">Continue with GitHub</span>
</button>

<div *ngIf="!isCustomDomain" class="login-form__sso-button-box">
<div *ngIf="isSaas && !isCustomDomain" class="login-form__sso-button-box">
<button type="button" mat-stroked-button color="primary"
data-testid="login-sso-button"
class="login-form__sso-button"
Expand Down
38 changes: 20 additions & 18 deletions frontend/src/app/components/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,25 +88,27 @@ export class LoginComponent implements OnInit, AfterViewInit {
}

ngAfterViewInit() {
const gAccounts: accounts = google.accounts;
gAccounts.id.initialize({
client_id: "681163285738-e4l0lrv5vv7m616ucrfhnhso9r396lum.apps.googleusercontent.com",
callback: (authUser) => {
this.ngZone.run(() => {
this._auth.loginWithGoogle(authUser.credential).subscribe((res) => {
if (res.isTemporary) this.is2FAShown = true;
this.angulartics2.eventTrack.next({
action: 'Login: google login success'
if (this.isSaas) {
const gAccounts: accounts = google.accounts;
gAccounts.id.initialize({
client_id: "681163285738-e4l0lrv5vv7m616ucrfhnhso9r396lum.apps.googleusercontent.com",
callback: (authUser) => {
this.ngZone.run(() => {
this._auth.loginWithGoogle(authUser.credential).subscribe((res) => {
if (res.isTemporary) this.is2FAShown = true;
this.angulartics2.eventTrack.next({
action: 'Login: google login success'
});
});
});
})
}
});
gAccounts.id.renderButton(
document.getElementById("google_login_button"),
{ theme: "outline", size: "large", width: 360, text: "continue_with" }
);
gAccounts.id.prompt();
})
}
});
gAccounts.id.renderButton(
document.getElementById("google_login_button"),
{ theme: "outline", size: "large", width: 360, text: "continue_with" }
);
gAccounts.id.prompt();
}
}

requestUserCompanies() {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/environments/environment.dev.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const environment = {
production: false,
saas: true,
saas: false,
apiRoot: "https://app.rocketadmin.com/api",
saasURL: "https://app.rocketadmin.com",
saasHostnames: ['localhost'],
Expand Down
Loading