Skip to content

Commit d87cb28

Browse files
guguclaude
andcommitted
feat: refactor widget structure and add chart label type option
- Refactor widget to only contain positioning data (position_x, position_y, width, height, query_id) - Move widget display properties (widget_type, chart_type, widget_options) to SavedQuery - Add dashboard-widget component to handle data fetching and widget rendering - Display widget name from saved query in widget header - Add drag and resize support for widgets in edit mode using angular-gridster2 v20 - Add "Label Type" option for bar/line charts with "Values" and "Datetime" options - Update all widget renderer tests to use new structure with preloaded data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4fa7f24 commit d87cb28

32 files changed

Lines changed: 858 additions & 468 deletions

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"@zxcvbn-ts/core": "^3.0.4",
3737
"@zxcvbn-ts/language-en": "^3.0.2",
3838
"amplitude-js": "^8.21.9",
39-
"angular-gridster2": "^21.0.1",
39+
"angular-gridster2": "^20.0.0",
4040
"angular-password-strength-meter": "npm:@eresearchqut/angular-password-strength-meter@^13.0.7",
4141
"angulartics2": "^14.1.0",
4242
"chart.js": "^4.5.1",

frontend/src/app/components/charts/chart-delete-dialog/chart-delete-dialog.component.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ describe('ChartDeleteDialogComponent', () => {
2525
id: '1',
2626
name: 'Test Query',
2727
description: 'Test description',
28+
widget_type: 'chart',
29+
chart_type: 'bar',
30+
widget_options: null,
2831
query_text: 'SELECT * FROM users',
2932
connection_id: 'conn-1',
3033
created_at: '2024-01-01',

frontend/src/app/components/charts/chart-edit/chart-edit.component.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ <h3>Chart Preview</h3>
8484
</mat-select>
8585
</mat-form-field>
8686

87+
<mat-form-field appearance="outline" class="chart-config-field" *ngIf="showLabelTypeOption()">
88+
<mat-label>Label Type</mat-label>
89+
<mat-select [ngModel]="labelType()" (ngModelChange)="labelType.set($event)" data-testid="label-type-select">
90+
<mat-option *ngFor="let type of labelTypes" [value]="type.value">
91+
{{ type.label }}
92+
</mat-option>
93+
</mat-select>
94+
</mat-form-field>
95+
8796
<mat-form-field appearance="outline" class="chart-config-field">
8897
<mat-label>Value Column</mat-label>
8998
<mat-select [ngModel]="valueColumn()" (ngModelChange)="valueColumn.set($event)" data-testid="value-column-select">
@@ -99,7 +108,8 @@ <h3>Chart Preview</h3>
99108
[chartType]="chartType()"
100109
[data]="testResults()"
101110
[labelColumn]="labelColumn()"
102-
[valueColumn]="valueColumn()">
111+
[valueColumn]="valueColumn()"
112+
[labelType]="labelType()">
103113
</app-chart-preview>
104114
</div>
105115

frontend/src/app/components/charts/chart-edit/chart-edit.component.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ describe('ChartEditComponent', () => {
4646
id: '1',
4747
name: 'Test Query',
4848
description: 'Test description',
49+
widget_type: 'chart',
50+
chart_type: 'bar',
51+
widget_options: { label_column: 'name', value_column: 'count' },
4952
query_text: 'SELECT * FROM users',
5053
connection_id: 'conn-1',
5154
created_at: '2024-01-01',
@@ -228,6 +231,9 @@ describe('ChartEditComponent', () => {
228231
name: 'New Query',
229232
description: undefined,
230233
query_text: 'SELECT 1',
234+
widget_type: 'chart',
235+
chart_type: 'bar',
236+
widget_options: undefined,
231237
});
232238
});
233239

frontend/src/app/components/charts/chart-edit/chart-edit.component.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export class ChartEditComponent implements OnInit {
6363
protected chartType = signal<ChartType>('bar');
6464
protected labelColumn = signal('');
6565
protected valueColumn = signal('');
66+
protected labelType = signal<'values' | 'datetime'>('values');
6667

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

76+
public labelTypes: { value: 'values' | 'datetime'; label: string }[] = [
77+
{ value: 'values', label: 'Values' },
78+
{ value: 'datetime', label: 'Datetime' },
79+
];
80+
81+
protected showLabelTypeOption = computed(() => ['bar', 'line'].includes(this.chartType()));
82+
7583
// Use a signal for codeModel to ensure change detection works on load
7684
// Only update this signal when loading a query, not during typing (to preserve cursor position)
7785
protected codeModel = signal({
@@ -140,6 +148,21 @@ export class ChartEditComponent implements OnInit {
140148
this.queryName.set(query.name);
141149
this.queryDescription.set(query.description || '');
142150
this.queryText.set(query.query_text);
151+
// Load chart configuration
152+
if (query.chart_type) {
153+
this.chartType.set(query.chart_type);
154+
}
155+
if (query.widget_options) {
156+
if (query.widget_options['label_column']) {
157+
this.labelColumn.set(query.widget_options['label_column'] as string);
158+
}
159+
if (query.widget_options['value_column']) {
160+
this.valueColumn.set(query.widget_options['value_column'] as string);
161+
}
162+
if (query.widget_options['label_type']) {
163+
this.labelType.set(query.widget_options['label_type'] as 'values' | 'datetime');
164+
}
165+
}
143166
// Set codeModel value for Monaco editor (only on load, not during typing)
144167
this.codeModel.set({ language: 'sql', uri: 'query.sql', value: query.query_text });
145168
// Automatically test the query to show chart preview
@@ -190,10 +213,25 @@ export class ChartEditComponent implements OnInit {
190213

191214
this.saving.set(true);
192215

216+
// Build widget_options with column selections
217+
const widgetOptions: Record<string, unknown> = {};
218+
if (this.labelColumn()) {
219+
widgetOptions['label_column'] = this.labelColumn();
220+
}
221+
if (this.valueColumn()) {
222+
widgetOptions['value_column'] = this.valueColumn();
223+
}
224+
if (this.labelType() && this.showLabelTypeOption()) {
225+
widgetOptions['label_type'] = this.labelType();
226+
}
227+
193228
const payload = {
194229
name: this.queryName(),
195230
description: this.queryDescription() || undefined,
196231
query_text: this.queryText(),
232+
widget_type: 'chart' as const,
233+
chart_type: this.chartType(),
234+
widget_options: Object.keys(widgetOptions).length > 0 ? widgetOptions : undefined,
197235
};
198236

199237
if (this.isEditMode()) {

frontend/src/app/components/charts/chart-preview/chart-preview.component.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class ChartPreviewComponent implements OnChanges {
1515
@Input() data: Record<string, unknown>[] = [];
1616
@Input() labelColumn = '';
1717
@Input() valueColumn = '';
18+
@Input() labelType: 'values' | 'datetime' = 'values';
1819

1920
public chartData: ChartData<ChartJsType> | null = null;
2021
public chartOptions: ChartConfiguration['options'] = {
@@ -42,7 +43,13 @@ export class ChartPreviewComponent implements OnChanges {
4243
];
4344

4445
ngOnChanges(changes: SimpleChanges): void {
45-
if (changes['data'] || changes['labelColumn'] || changes['valueColumn'] || changes['chartType']) {
46+
if (
47+
changes['data'] ||
48+
changes['labelColumn'] ||
49+
changes['valueColumn'] ||
50+
changes['chartType'] ||
51+
changes['labelType']
52+
) {
4653
this.updateChartData();
4754
}
4855
}
@@ -57,7 +64,13 @@ export class ChartPreviewComponent implements OnChanges {
5764
return;
5865
}
5966

60-
const labels = this.data.map((row) => String(row[this.labelColumn] ?? ''));
67+
const labels = this.data.map((row) => {
68+
const val = row[this.labelColumn];
69+
if (this.labelType === 'datetime' && val) {
70+
return this.formatDatetime(val);
71+
}
72+
return String(val ?? '');
73+
});
6174
const values = this.data.map((row) => {
6275
const val = row[this.valueColumn];
6376
return typeof val === 'number' ? val : parseFloat(String(val)) || 0;
@@ -93,4 +106,23 @@ export class ChartPreviewComponent implements OnChanges {
93106
};
94107
}
95108
}
109+
110+
private formatDatetime(value: unknown): string {
111+
if (!value) return '';
112+
113+
try {
114+
const date = new Date(value as string | number | Date);
115+
if (isNaN(date.getTime())) {
116+
return String(value);
117+
}
118+
// Format as localized date string
119+
return date.toLocaleDateString(undefined, {
120+
year: 'numeric',
121+
month: 'short',
122+
day: 'numeric',
123+
});
124+
} catch {
125+
return String(value);
126+
}
127+
}
96128
}

frontend/src/app/components/charts/charts-list/charts-list.component.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ describe('ChartsListComponent', () => {
3636
id: '1',
3737
name: 'Test Query',
3838
description: 'Test description',
39+
widget_type: 'chart',
40+
chart_type: 'bar',
41+
widget_options: null,
3942
query_text: 'SELECT * FROM users',
4043
connection_id: 'conn-1',
4144
created_at: '2024-01-01',

0 commit comments

Comments
 (0)