Skip to content

Commit ca7690c

Browse files
guguclaude
andcommitted
feat: add frontend charts/saved queries feature
- Add chart.js and ng2-charts dependencies - Create saved-query models and SavedQueriesService - Add charts-list component with search and delete functionality - Add chart-edit component with Monaco SQL editor and chart preview - Add chart-preview component using Chart.js (bar, line, pie, doughnut, polar area) - Add chart-delete-dialog component - Add charts tab to navigation for all connections - Add routes for /charts/:connection-id, /new, and /:query-id Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e16fca3 commit ca7690c

23 files changed

Lines changed: 2654 additions & 564 deletions

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"amplitude-js": "^8.21.9",
3939
"angular-password-strength-meter": "npm:@eresearchqut/angular-password-strength-meter@^13.0.7",
4040
"angulartics2": "^14.1.0",
41+
"chart.js": "^4.5.1",
4142
"color-string": "^2.0.1",
4243
"convert": "^5.12.0",
4344
"date-fns": "^4.1.0",
@@ -50,6 +51,7 @@
5051
"mermaid": "^11.12.1",
5152
"monaco-editor": "0.55.1",
5253
"ng-dynamic-component": "^10.7.0",
54+
"ng2-charts": "^8.0.0",
5355
"ngx-cookie-service": "^19.0.0",
5456
"ngx-markdown": "^19.1.1",
5557
"ngx-stripe": "^19.0.0",
Lines changed: 211 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,219 @@
1+
import { NgModule } from '@angular/core';
12
import { RouterModule, Routes } from '@angular/router';
2-
33
import { AuthGuard } from './auth.guard';
4-
import { NgModule } from '@angular/core';
54

65
const routes: Routes = [
7-
{path: '', redirectTo: '/connections-list', pathMatch: 'full'},
8-
{path: 'loader', loadComponent: () => import('./components/page-loader/page-loader.component').then(m => m.PageLoaderComponent)},
9-
{path: 'registration', loadChildren: () => import('./routes/registration.routes').then(m => m.REGISTRATION_ROUTES)},
10-
{path: 'login', loadComponent: () => import('./components/login/login.component').then(m => m.LoginComponent), title: 'Login | Rocketadmin'},
11-
{path: 'forget-password', loadComponent: () => import('./components/password-request/password-request.component').then(m => m.PasswordRequestComponent), title: 'Request password | Rocketadmin'},
12-
{path: 'external/user/password/reset/verify/:verification-token', loadChildren: () => import('./routes/password-reset.routes').then(m => m.PASSWORD_RESET_ROUTES)},
13-
{path: 'external/user/email/verify/:verification-token', loadComponent: () => import('./components/email-verification/email-verification.component').then(m => m.EmailVerificationComponent), title: 'Email verification | Rocketadmin'},
14-
{path: 'external/user/email/change/verify/:change-token', loadComponent: () => import('./components/email-change/email-change.component').then(m => m.EmailChangeComponent), title: 'Email updating | Rocketadmin'},
15-
{path: 'deleted', loadComponent: () => import('./components/user-deleted-success/user-deleted-success.component').then(m => m.UserDeletedSuccessComponent), title: 'User deleted | Rocketadmin'},
16-
{path: 'connect-db', loadComponent: () => import('./components/connect-db/connect-db.component').then(m => m.ConnectDBComponent), canActivate: [AuthGuard]},
17-
{path: 'connections-list', loadComponent: () => import('./components/connections-list/connections-list.component').then(m => m.ConnectionsListComponent), canActivate: [AuthGuard]},
18-
{path: 'user-settings', loadComponent: () => import('./components/user-settings/user-settings.component').then(m => m.UserSettingsComponent), canActivate: [AuthGuard]},
19-
// company routes have to be in this specific order
20-
{path: 'company/:company-id/verify/:verification-token', pathMatch: 'full', loadChildren: () => import('./routes/company-invitation.routes').then(m => m.COMPANY_INVITATION_ROUTES)},
21-
{path: 'company', pathMatch: 'full', loadComponent: () => import('./components/company/company.component').then(m => m.CompanyComponent), canActivate: [AuthGuard]},
22-
{path: 'secrets', pathMatch: 'full', loadComponent: () => import('./components/secrets/secrets.component').then(m => m.SecretsComponent), canActivate: [AuthGuard], title: 'Secrets | Rocketadmin'},
23-
{path: 'sso/:company-id', pathMatch: 'full', loadComponent: () => import('./components/sso/sso.component').then(m => m.SsoComponent), canActivate: [AuthGuard]},
24-
{path: 'change-password', loadChildren: () => import('./routes/password-change.routes').then(m => m.PASSWORD_CHANGE_ROUTES)},
25-
{path: 'upgrade', loadComponent: () => import('./components/upgrade/upgrade.component').then(m => m.UpgradeComponent), canActivate: [AuthGuard], title: 'Upgrade | Rocketadmin'},
26-
{path: 'upgrade/payment', loadComponent: () => import('./components/payment-form/payment-form.component').then(m => m.PaymentFormComponent), canActivate: [AuthGuard], title: 'Payment | Rocketadmin'},
27-
{path: 'subscription/success', loadComponent: () => import('./components/upgrade-success/upgrade-success.component').then(m => m.UpgradeSuccessComponent), canActivate: [AuthGuard], title: 'Upgraded successfully | Rocketadmin'},
28-
{path: 'edit-db/:connection-id', loadComponent: () => import('./components/connect-db/connect-db.component').then(m => m.ConnectDBComponent), canActivate: [AuthGuard]},
29-
{path: 'connection-settings/:connection-id', loadComponent: () => import('./components/connection-settings/connection-settings.component').then(m => m.ConnectionSettingsComponent), canActivate: [AuthGuard]},
30-
{path: 'dashboard/:connection-id', loadComponent: () => import('./components/dashboard/dashboard.component').then(m => m.DashboardComponent), canActivate: [AuthGuard]},
31-
{path: 'audit/:connection-id', loadComponent: () => import('./components/audit/audit.component').then(m => m.AuditComponent), canActivate: [AuthGuard]},
32-
{path: 'dashboard/:connection-id/:table-name', pathMatch: 'full', loadComponent: () => import('./components/dashboard/dashboard.component').then(m => m.DashboardComponent), canActivate: [AuthGuard]},
33-
{path: 'dashboard/:connection-id/:table-name/entry', pathMatch: 'full', loadComponent: () => import('./components/db-table-row-edit/db-table-row-edit.component').then(m => m.DbTableRowEditComponent), canActivate: [AuthGuard]},
34-
{path: 'dashboard/:connection-id/:table-name/widgets', pathMatch: 'full', loadComponent: () => import('./components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component').then(m => m.DbTableWidgetsComponent), canActivate: [AuthGuard]},
35-
{path: 'dashboard/:connection-id/:table-name/settings', pathMatch: 'full', loadComponent: () => import('./components/dashboard/db-table-view/db-table-settings/db-table-settings.component').then(m => m.DbTableSettingsComponent), canActivate: [AuthGuard]},
36-
{path: 'dashboard/:connection-id/:table-name/actions', pathMatch: 'full', loadComponent: () => import('./components/dashboard/db-table-view/db-table-actions/db-table-actions.component').then(m => m.DbTableActionsComponent), canActivate: [AuthGuard]},
37-
{path: 'permissions/:connection-id', loadComponent: () => import('./components/users/users.component').then(m => m.UsersComponent), canActivate: [AuthGuard]},
38-
{path: 'zapier', loadComponent: () => import('./components/zapier/zapier.component').then(m => m.ZapierComponent), canActivate: [AuthGuard]},
39-
{path: '**', loadComponent: () => import('./components/page-not-found/page-not-found.component').then(m => m.PageNotFoundComponent)},
6+
{ path: '', redirectTo: '/connections-list', pathMatch: 'full' },
7+
{
8+
path: 'loader',
9+
loadComponent: () => import('./components/page-loader/page-loader.component').then((m) => m.PageLoaderComponent),
10+
},
11+
{
12+
path: 'registration',
13+
loadChildren: () => import('./routes/registration.routes').then((m) => m.REGISTRATION_ROUTES),
14+
},
15+
{
16+
path: 'login',
17+
loadComponent: () => import('./components/login/login.component').then((m) => m.LoginComponent),
18+
title: 'Login | Rocketadmin',
19+
},
20+
{
21+
path: 'forget-password',
22+
loadComponent: () =>
23+
import('./components/password-request/password-request.component').then((m) => m.PasswordRequestComponent),
24+
title: 'Request password | Rocketadmin',
25+
},
26+
{
27+
path: 'external/user/password/reset/verify/:verification-token',
28+
loadChildren: () => import('./routes/password-reset.routes').then((m) => m.PASSWORD_RESET_ROUTES),
29+
},
30+
{
31+
path: 'external/user/email/verify/:verification-token',
32+
loadComponent: () =>
33+
import('./components/email-verification/email-verification.component').then((m) => m.EmailVerificationComponent),
34+
title: 'Email verification | Rocketadmin',
35+
},
36+
{
37+
path: 'external/user/email/change/verify/:change-token',
38+
loadComponent: () => import('./components/email-change/email-change.component').then((m) => m.EmailChangeComponent),
39+
title: 'Email updating | Rocketadmin',
40+
},
41+
{
42+
path: 'deleted',
43+
loadComponent: () =>
44+
import('./components/user-deleted-success/user-deleted-success.component').then(
45+
(m) => m.UserDeletedSuccessComponent,
46+
),
47+
title: 'User deleted | Rocketadmin',
48+
},
49+
{
50+
path: 'connect-db',
51+
loadComponent: () => import('./components/connect-db/connect-db.component').then((m) => m.ConnectDBComponent),
52+
canActivate: [AuthGuard],
53+
},
54+
{
55+
path: 'connections-list',
56+
loadComponent: () =>
57+
import('./components/connections-list/connections-list.component').then((m) => m.ConnectionsListComponent),
58+
canActivate: [AuthGuard],
59+
},
60+
{
61+
path: 'user-settings',
62+
loadComponent: () =>
63+
import('./components/user-settings/user-settings.component').then((m) => m.UserSettingsComponent),
64+
canActivate: [AuthGuard],
65+
},
66+
// company routes have to be in this specific order
67+
{
68+
path: 'company/:company-id/verify/:verification-token',
69+
pathMatch: 'full',
70+
loadChildren: () => import('./routes/company-invitation.routes').then((m) => m.COMPANY_INVITATION_ROUTES),
71+
},
72+
{
73+
path: 'company',
74+
pathMatch: 'full',
75+
loadComponent: () => import('./components/company/company.component').then((m) => m.CompanyComponent),
76+
canActivate: [AuthGuard],
77+
},
78+
{
79+
path: 'secrets',
80+
pathMatch: 'full',
81+
loadComponent: () => import('./components/secrets/secrets.component').then((m) => m.SecretsComponent),
82+
canActivate: [AuthGuard],
83+
title: 'Secrets | Rocketadmin',
84+
},
85+
{
86+
path: 'sso/:company-id',
87+
pathMatch: 'full',
88+
loadComponent: () => import('./components/sso/sso.component').then((m) => m.SsoComponent),
89+
canActivate: [AuthGuard],
90+
},
91+
{
92+
path: 'change-password',
93+
loadChildren: () => import('./routes/password-change.routes').then((m) => m.PASSWORD_CHANGE_ROUTES),
94+
},
95+
{
96+
path: 'upgrade',
97+
loadComponent: () => import('./components/upgrade/upgrade.component').then((m) => m.UpgradeComponent),
98+
canActivate: [AuthGuard],
99+
title: 'Upgrade | Rocketadmin',
100+
},
101+
{
102+
path: 'upgrade/payment',
103+
loadComponent: () => import('./components/payment-form/payment-form.component').then((m) => m.PaymentFormComponent),
104+
canActivate: [AuthGuard],
105+
title: 'Payment | Rocketadmin',
106+
},
107+
{
108+
path: 'subscription/success',
109+
loadComponent: () =>
110+
import('./components/upgrade-success/upgrade-success.component').then((m) => m.UpgradeSuccessComponent),
111+
canActivate: [AuthGuard],
112+
title: 'Upgraded successfully | Rocketadmin',
113+
},
114+
{
115+
path: 'edit-db/:connection-id',
116+
loadComponent: () => import('./components/connect-db/connect-db.component').then((m) => m.ConnectDBComponent),
117+
canActivate: [AuthGuard],
118+
},
119+
{
120+
path: 'connection-settings/:connection-id',
121+
loadComponent: () =>
122+
import('./components/connection-settings/connection-settings.component').then(
123+
(m) => m.ConnectionSettingsComponent,
124+
),
125+
canActivate: [AuthGuard],
126+
},
127+
{
128+
path: 'dashboard/:connection-id',
129+
loadComponent: () => import('./components/dashboard/dashboard.component').then((m) => m.DashboardComponent),
130+
canActivate: [AuthGuard],
131+
},
132+
{
133+
path: 'audit/:connection-id',
134+
loadComponent: () => import('./components/audit/audit.component').then((m) => m.AuditComponent),
135+
canActivate: [AuthGuard],
136+
},
137+
{
138+
path: 'dashboard/:connection-id/:table-name',
139+
pathMatch: 'full',
140+
loadComponent: () => import('./components/dashboard/dashboard.component').then((m) => m.DashboardComponent),
141+
canActivate: [AuthGuard],
142+
},
143+
{
144+
path: 'dashboard/:connection-id/:table-name/entry',
145+
pathMatch: 'full',
146+
loadComponent: () =>
147+
import('./components/db-table-row-edit/db-table-row-edit.component').then((m) => m.DbTableRowEditComponent),
148+
canActivate: [AuthGuard],
149+
},
150+
{
151+
path: 'dashboard/:connection-id/:table-name/widgets',
152+
pathMatch: 'full',
153+
loadComponent: () =>
154+
import('./components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component').then(
155+
(m) => m.DbTableWidgetsComponent,
156+
),
157+
canActivate: [AuthGuard],
158+
},
159+
{
160+
path: 'dashboard/:connection-id/:table-name/settings',
161+
pathMatch: 'full',
162+
loadComponent: () =>
163+
import('./components/dashboard/db-table-view/db-table-settings/db-table-settings.component').then(
164+
(m) => m.DbTableSettingsComponent,
165+
),
166+
canActivate: [AuthGuard],
167+
},
168+
{
169+
path: 'dashboard/:connection-id/:table-name/actions',
170+
pathMatch: 'full',
171+
loadComponent: () =>
172+
import('./components/dashboard/db-table-view/db-table-actions/db-table-actions.component').then(
173+
(m) => m.DbTableActionsComponent,
174+
),
175+
canActivate: [AuthGuard],
176+
},
177+
{
178+
path: 'permissions/:connection-id',
179+
loadComponent: () => import('./components/users/users.component').then((m) => m.UsersComponent),
180+
canActivate: [AuthGuard],
181+
},
182+
{
183+
path: 'zapier',
184+
loadComponent: () => import('./components/zapier/zapier.component').then((m) => m.ZapierComponent),
185+
canActivate: [AuthGuard],
186+
},
187+
{
188+
path: 'charts/:connection-id',
189+
loadComponent: () =>
190+
import('./components/charts/charts-list/charts-list.component').then((m) => m.ChartsListComponent),
191+
canActivate: [AuthGuard],
192+
title: 'Saved Queries | Rocketadmin',
193+
},
194+
{
195+
path: 'charts/:connection-id/new',
196+
loadComponent: () =>
197+
import('./components/charts/chart-edit/chart-edit.component').then((m) => m.ChartEditComponent),
198+
canActivate: [AuthGuard],
199+
title: 'Create Query | Rocketadmin',
200+
},
201+
{
202+
path: 'charts/:connection-id/:query-id',
203+
loadComponent: () =>
204+
import('./components/charts/chart-edit/chart-edit.component').then((m) => m.ChartEditComponent),
205+
canActivate: [AuthGuard],
206+
title: 'Edit Query | Rocketadmin',
207+
},
208+
{
209+
path: '**',
210+
loadComponent: () =>
211+
import('./components/page-not-found/page-not-found.component').then((m) => m.PageNotFoundComponent),
212+
},
40213
];
41214

42215
@NgModule({
43-
imports: [RouterModule.forRoot(routes)],
44-
exports: [RouterModule]
216+
imports: [RouterModule.forRoot(routes)],
217+
exports: [RouterModule],
45218
})
46-
export class AppRoutingModule { }
219+
export class AppRoutingModule {}

frontend/src/app/app.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ export class AppComponent {
194194
permissions: {
195195
caption: 'Permissions',
196196
},
197+
charts: {
198+
caption: 'Charts',
199+
},
197200
'connection-settings': {
198201
caption: 'Connection settings',
199202
},
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
.warning-container {
2+
display: flex;
3+
gap: 16px;
4+
padding: 16px;
5+
background-color: #fff3e0;
6+
border-radius: 8px;
7+
}
8+
9+
@media (prefers-color-scheme: dark) {
10+
.warning-container {
11+
background-color: rgba(255, 152, 0, 0.15);
12+
}
13+
}
14+
15+
.warning-icon {
16+
font-size: 32px;
17+
width: 32px;
18+
height: 32px;
19+
color: #ef6c00;
20+
flex-shrink: 0;
21+
}
22+
23+
@media (prefers-color-scheme: dark) {
24+
.warning-icon {
25+
color: #ffb74d;
26+
}
27+
}
28+
29+
.warning-content p {
30+
margin: 0;
31+
}
32+
33+
.warning-content p:first-child {
34+
margin-bottom: 8px;
35+
}
36+
37+
.warning-details {
38+
font-size: 14px;
39+
color: rgba(0, 0, 0, 0.54);
40+
}
41+
42+
@media (prefers-color-scheme: dark) {
43+
.warning-details {
44+
color: rgba(255, 255, 255, 0.54);
45+
}
46+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<h2 mat-dialog-title>Delete Saved Query</h2>
2+
3+
<mat-dialog-content>
4+
<div class="warning-container">
5+
<mat-icon class="warning-icon">warning</mat-icon>
6+
<div class="warning-content">
7+
<p>Are you sure you want to delete the query <strong>{{data.query.name}}</strong>?</p>
8+
<p class="warning-details">
9+
This action cannot be undone. The query and its configuration will be permanently removed.
10+
</p>
11+
</div>
12+
</div>
13+
</mat-dialog-content>
14+
15+
<mat-dialog-actions align="end">
16+
<button mat-button mat-dialog-close [disabled]="submitting">Cancel</button>
17+
<button mat-flat-button color="warn"
18+
(click)="onDelete()"
19+
[disabled]="submitting"
20+
data-testid="delete-chart-confirm-button">
21+
{{submitting ? 'Deleting...' : 'Delete Query'}}
22+
</button>
23+
</mat-dialog-actions>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { CommonModule } from '@angular/common';
2+
import { Component, Inject } from '@angular/core';
3+
import { MatButtonModule } from '@angular/material/button';
4+
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
5+
import { MatIconModule } from '@angular/material/icon';
6+
import { Angulartics2 } from 'angulartics2';
7+
import { SavedQuery } from 'src/app/models/saved-query';
8+
import { SavedQueriesService } from 'src/app/services/saved-queries.service';
9+
10+
@Component({
11+
selector: 'app-chart-delete-dialog',
12+
templateUrl: './chart-delete-dialog.component.html',
13+
styleUrls: ['./chart-delete-dialog.component.css'],
14+
imports: [CommonModule, MatDialogModule, MatButtonModule, MatIconModule],
15+
})
16+
export class ChartDeleteDialogComponent {
17+
public submitting = false;
18+
19+
constructor(
20+
@Inject(MAT_DIALOG_DATA) public data: { query: SavedQuery; connectionId: string },
21+
private dialogRef: MatDialogRef<ChartDeleteDialogComponent>,
22+
private _savedQueries: SavedQueriesService,
23+
private angulartics2: Angulartics2,
24+
) {}
25+
26+
onDelete(): void {
27+
this.submitting = true;
28+
this._savedQueries.deleteSavedQuery(this.data.connectionId, this.data.query.id).subscribe({
29+
next: () => {
30+
this.angulartics2.eventTrack.next({
31+
action: 'Charts: saved query deleted successfully',
32+
});
33+
this.submitting = false;
34+
this.dialogRef.close(true);
35+
},
36+
error: () => {
37+
this.submitting = false;
38+
},
39+
});
40+
}
41+
}

0 commit comments

Comments
 (0)