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: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
"amplitude-js": "^8.21.9",
"angular-gridster2": "^20.0.0",
"angular-password-strength-meter": "npm:@eresearchqut/angular-password-strength-meter@^13.0.7",
"angulartics2": "^14.1.0",
"chart.js": "^4.5.1",
"chartjs-adapter-date-fns": "^3.0.0",
"color-string": "^2.0.1",
"convert": "^5.12.0",
"date-fns": "^4.1.0",
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@ const routes: Routes = [
canActivate: [AuthGuard],
title: 'Edit Query | Rocketadmin',
},
{
path: 'dashboards/:connection-id',
loadComponent: () =>
import('./components/dashboards/dashboards-list/dashboards-list.component').then(
(m) => m.DashboardsListComponent,
),
canActivate: [AuthGuard],
title: 'Dashboards | Rocketadmin',
},
{
path: 'dashboards/:connection-id/:dashboard-id',
loadComponent: () =>
import('./components/dashboards/dashboard-view/dashboard-view.component').then((m) => m.DashboardViewComponent),
canActivate: [AuthGuard],
title: 'Dashboard | Rocketadmin',
},
{
path: '**',
loadComponent: () =>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ export class AppComponent {
permissions: {
caption: 'Permissions',
},
charts: {
caption: 'Charts',
dashboards: {
caption: 'Dashboards',
},
'connection-settings': {
caption: 'Connection settings',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ describe('ChartDeleteDialogComponent', () => {
id: '1',
name: 'Test Query',
description: 'Test description',
widget_type: 'chart',
chart_type: 'bar',
widget_options: null,
query_text: 'SELECT * FROM users',
connection_id: 'conn-1',
created_at: '2024-01-01',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@
}

.editor-section,
.right-panel {
display: flex;
flex-direction: column;
gap: 24px;
}

.config-section,
.preview-section {
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,54 +57,72 @@ <h3>SQL Query</h3>
</div>
</div>

<div class="preview-section" *ngIf="showResults()">
<div class="section-header">
<h3>Chart Preview</h3>
<span class="execution-time" *ngIf="executionTime() !== null">
Executed in {{ executionTime() }}ms
</span>
</div>
<div class="right-panel" *ngIf="showResults()">
<div class="config-section">
<div class="section-header">
<h3>Chart Configuration</h3>
<span class="execution-time" *ngIf="executionTime() !== null">
Executed in {{ executionTime() }}ms
</span>
</div>

<div class="chart-config">
<mat-form-field appearance="outline" class="chart-config-field">
<mat-label>Chart Type</mat-label>
<mat-select [ngModel]="chartType()" (ngModelChange)="chartType.set($event)" data-testid="chart-type-select">
<mat-option *ngFor="let type of chartTypes" [value]="type.value">
{{ type.label }}
</mat-option>
</mat-select>
</mat-form-field>

<mat-form-field appearance="outline" class="chart-config-field">
<mat-label>Label Column</mat-label>
<mat-select [ngModel]="labelColumn()" (ngModelChange)="labelColumn.set($event)" data-testid="label-column-select">
<mat-option *ngFor="let col of resultColumns()" [value]="col">
{{ col }}
</mat-option>
</mat-select>
</mat-form-field>

<mat-form-field appearance="outline" class="chart-config-field">
<mat-label>Value Column</mat-label>
<mat-select [ngModel]="valueColumn()" (ngModelChange)="valueColumn.set($event)" data-testid="value-column-select">
<mat-option *ngFor="let col of resultColumns()" [value]="col">
{{ col }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="chart-config">
<mat-form-field appearance="outline" class="chart-config-field">
<mat-label>Chart Type</mat-label>
<mat-select [ngModel]="chartType()" (ngModelChange)="chartType.set($event)" data-testid="chart-type-select">
<mat-option *ngFor="let type of chartTypes" [value]="type.value">
{{ type.label }}
</mat-option>
</mat-select>
</mat-form-field>

<mat-form-field appearance="outline" class="chart-config-field">
<mat-label>Label Column</mat-label>
<mat-select [ngModel]="labelColumn()" (ngModelChange)="labelColumn.set($event)" data-testid="label-column-select">
<mat-option *ngFor="let col of resultColumns()" [value]="col">
{{ col }}
</mat-option>
</mat-select>
</mat-form-field>

<div class="chart-container" *ngIf="hasChartData()">
<app-chart-preview
[chartType]="chartType()"
[data]="testResults()"
[labelColumn]="labelColumn()"
[valueColumn]="valueColumn()">
</app-chart-preview>
<mat-form-field appearance="outline" class="chart-config-field" *ngIf="showLabelTypeOption()">
<mat-label>Label Type</mat-label>
<mat-select [ngModel]="labelType()" (ngModelChange)="labelType.set($event)" data-testid="label-type-select">
<mat-option *ngFor="let type of labelTypes" [value]="type.value">
{{ type.label }}
</mat-option>
</mat-select>
</mat-form-field>

<mat-form-field appearance="outline" class="chart-config-field">
<mat-label>Value Column</mat-label>
<mat-select [ngModel]="valueColumn()" (ngModelChange)="valueColumn.set($event)" data-testid="value-column-select">
<mat-option *ngFor="let col of resultColumns()" [value]="col">
{{ col }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>

<div class="no-chart-data" *ngIf="!hasChartData() && testResults().length > 0">
<p>Select label and value columns to display the chart</p>
<div class="preview-section">
<div class="section-header">
<h3>Chart Preview</h3>
</div>

<div class="chart-container" *ngIf="hasChartData()">
<app-chart-preview
[chartType]="chartType()"
[data]="testResults()"
[labelColumn]="labelColumn()"
[valueColumn]="valueColumn()"
[labelType]="labelType()">
</app-chart-preview>
</div>

<div class="no-chart-data" *ngIf="!hasChartData() && testResults().length > 0">
<p>Select label and value columns to display the chart</p>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ describe('ChartEditComponent', () => {
id: '1',
name: 'Test Query',
description: 'Test description',
widget_type: 'chart',
chart_type: 'bar',
widget_options: { label_column: 'name', value_column: 'count' },
query_text: 'SELECT * FROM users',
connection_id: 'conn-1',
created_at: '2024-01-01',
Expand Down Expand Up @@ -228,6 +231,9 @@ describe('ChartEditComponent', () => {
name: 'New Query',
description: undefined,
query_text: 'SELECT 1',
widget_type: 'chart',
chart_type: 'bar',
widget_options: { label_type: 'values' },
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class ChartEditComponent implements OnInit {
protected chartType = signal<ChartType>('bar');
protected labelColumn = signal('');
protected valueColumn = signal('');
protected labelType = signal<'values' | 'datetime'>('values');

public chartTypes: { value: ChartType; label: string }[] = [
{ value: 'bar', label: 'Bar Chart' },
Expand All @@ -72,6 +73,13 @@ export class ChartEditComponent implements OnInit {
{ value: 'polarArea', label: 'Polar Area Chart' },
];

public labelTypes: { value: 'values' | 'datetime'; label: string }[] = [
{ value: 'values', label: 'Values' },
{ value: 'datetime', label: 'Datetime' },
];

protected showLabelTypeOption = computed(() => ['bar', 'line'].includes(this.chartType()));

// Use a signal for codeModel to ensure change detection works on load
// Only update this signal when loading a query, not during typing (to preserve cursor position)
protected codeModel = signal({
Expand Down Expand Up @@ -140,6 +148,21 @@ export class ChartEditComponent implements OnInit {
this.queryName.set(query.name);
this.queryDescription.set(query.description || '');
this.queryText.set(query.query_text);
// Load chart configuration
if (query.chart_type) {
this.chartType.set(query.chart_type);
}
if (query.widget_options) {
if (query.widget_options['label_column']) {
this.labelColumn.set(query.widget_options['label_column'] as string);
}
if (query.widget_options['value_column']) {
this.valueColumn.set(query.widget_options['value_column'] as string);
}
if (query.widget_options['label_type']) {
this.labelType.set(query.widget_options['label_type'] as 'values' | 'datetime');
}
}
// Set codeModel value for Monaco editor (only on load, not during typing)
this.codeModel.set({ language: 'sql', uri: 'query.sql', value: query.query_text });
// Automatically test the query to show chart preview
Expand Down Expand Up @@ -190,10 +213,25 @@ export class ChartEditComponent implements OnInit {

this.saving.set(true);

// Build widget_options with column selections
const widgetOptions: Record<string, unknown> = {};
if (this.labelColumn()) {
widgetOptions['label_column'] = this.labelColumn();
}
if (this.valueColumn()) {
widgetOptions['value_column'] = this.valueColumn();
}
if (this.labelType() && this.showLabelTypeOption()) {
widgetOptions['label_type'] = this.labelType();
}

const payload = {
name: this.queryName(),
description: this.queryDescription() || undefined,
query_text: this.queryText(),
widget_type: 'chart' as const,
chart_type: this.chartType(),
widget_options: Object.keys(widgetOptions).length > 0 ? widgetOptions : undefined,
};

if (this.isEditMode()) {
Expand Down
Loading
Loading