feat: add AI-driven table dashboard generation with customizable options#1631
feat: add AI-driven table dashboard generation with customizable options#1631
Conversation
Artuomka
commented
Feb 25, 2026
- Implemented GenerateTableDashboardWithAiDto for input validation.
- Created GenerateTableDashboardWithAiUseCase to handle AI-driven dashboard generation.
- Added integration tests for dashboard generation, including success and failure scenarios.
- Ensured AI-generated panels are validated for safety before persistence.
- Supported custom dashboard naming and handling of non-existent tables.
- Implemented GenerateTableDashboardWithAiDto for input validation. - Created GenerateTableDashboardWithAiUseCase to handle AI-driven dashboard generation. - Added integration tests for dashboard generation, including success and failure scenarios. - Ensured AI-generated panels are validated for safety before persistence. - Supported custom dashboard naming and handling of non-existent tables.
There was a problem hiding this comment.
Pull request overview
Adds a new backend capability to generate and persist an AI-created dashboard for a given table, plus refactors the existing AI “widget” generation to use “panel” terminology in responses and tests.
Changes:
- Introduces
GenerateTableDashboardWithAiUseCaseand related DTO/DS wiring to generate a dashboard + panels via AI and persist them. - Adds new SAAS and non-SAAS E2E tests covering success and failure scenarios for table-dashboard generation.
- Renames AI-generated response fields in the existing widget-generation flow from
widget_*topanel_*and updates tests accordingly.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts | Updates AI widget E2E assertions/mocks to panel_type/panel_options. |
| backend/test/ava-tests/saas-tests/dashboard-ai-generate-table-dashboard-e2e.test.ts | New SAAS E2E coverage for generating/persisting AI dashboards from a table. |
| backend/test/ava-tests/non-saas-tests/non-saas-dashboard-ai-generate-table-dashboard-e2e.test.ts | New non-SAAS E2E coverage for generating/persisting AI dashboards from a table. |
| backend/src/entities/visualizations/panel-position/use-cases/panel-position-use-cases.interface.ts | Adds IGenerateTableDashboardWithAi interface contract. |
| backend/src/entities/visualizations/panel-position/use-cases/generate-table-dashboard-with-ai.use.case.ts | Implements AI-driven dashboard suggestion, panel generation, safety checks, and persistence. |
| backend/src/entities/visualizations/panel-position/use-cases/generate-panel-position-with-ai.use.case.ts | Refactors AI widget generation to use panel_* naming and improved query response cleaning. |
| backend/src/entities/visualizations/panel-position/panel-position.module.ts | Registers new use case provider and route middleware configuration. |
| backend/src/entities/visualizations/panel-position/panel-position.controller.ts | Adds /dashboard/generate-table-dashboard/:connectionId endpoint and updates AI-generate docs/DTO usage. |
| backend/src/entities/visualizations/panel-position/dto/generated-panel-with-position.dto.ts | Changes generated response DTO field names to panel_type/panel_options and marks dashboard_id nullable. |
| backend/src/entities/visualizations/panel-position/dto/generate-table-dashboard-with-ai.dto.ts | New request DTO for max panels and optional dashboard name. |
| backend/src/entities/visualizations/panel-position/data-structures/generate-table-dashboard-with-ai.ds.ts | New DS for table-dashboard AI generation use case input. |
| backend/src/common/data-injection.tokens.ts | Adds DI token for the new generate-table-dashboard use case. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| break; | ||
| } | ||
|
|
||
| this.logger.log(`Query corrected by AI after EXPLAIN (iteration ${iteration + 1})`); | ||
| currentQuery = correctedQuery; | ||
|
|
||
| if (explainResult.success) { | ||
| break; | ||
| } |
There was a problem hiding this comment.
If the AI changes the query, the code may break out when the previous EXPLAIN succeeded (if (explainResult.success) break;) without re-running EXPLAIN on the modified query. That can allow an invalid query to be accepted after AI modification. Re-run EXPLAIN after any change (or only allow modifications when the previous EXPLAIN failed).
| break; | |
| } | |
| this.logger.log(`Query corrected by AI after EXPLAIN (iteration ${iteration + 1})`); | |
| currentQuery = correctedQuery; | |
| if (explainResult.success) { | |
| break; | |
| } | |
| if (explainResult.success) { | |
| break; | |
| } | |
| // EXPLAIN failed and AI did not change the query; allow further iterations | |
| continue; | |
| } | |
| this.logger.log(`Query corrected by AI after EXPLAIN (iteration ${iteration + 1})`); | |
| currentQuery = correctedQuery; | |
| // Do not break here: the next iteration will re-run EXPLAIN on the modified query |
| @ApiProperty({ description: 'Panel type', enum: DashboardWidgetTypeEnum }) | ||
| panel_type: DashboardWidgetTypeEnum; | ||
|
|
||
| @ApiPropertyOptional({ description: 'Chart type for chart widgets' }) | ||
| @ApiPropertyOptional({ description: 'Chart type for chart panels' }) | ||
| chart_type: string | null; |
There was a problem hiding this comment.
GeneratedPanelWithPositionDto now exposes panel_type/panel_options, while other visualization APIs still expose widget_type/widget_options (e.g., saved query DTOs). This makes /dashboard/:dashboardId/widget/generate/:connectionId inconsistent and is a breaking response change for existing clients. Consider keeping the existing property names (or returning both sets temporarily) to avoid breaking consumers and to keep naming consistent across endpoints.
| @SlugUuid('connectionId') connectionId: string, | ||
| @Query('tableName') tableName: string, | ||
| @MasterPassword() masterPwd: string, | ||
| @UserId() userId: string, | ||
| @Body() generateDto: GenerateTableDashboardWithAiDto, |
There was a problem hiding this comment.
The required tableName query parameter is not validated before being used to build the DS. This currently relies on downstream DAO failures to produce a 400, which can yield unclear messages. Add an explicit check (or a validation pipe) to return a BadRequestException when tableName is missing/empty.
| max_panels: generateDto.max_panels, | ||
| dashboard_name: generateDto.dashboard_name, | ||
| }; | ||
| return await this.generateTableDashboardWithAiUseCase.execute(inputData, InTransactionEnum.ON); |
There was a problem hiding this comment.
This endpoint executes the use case with InTransactionEnum.ON, which starts a global DB transaction before the use case runs. Since the use case performs multiple AI calls (and potentially multiple EXPLAIN calls) before persisting anything, this can keep a transaction open for a long time and increase lock/connection contention. Prefer running the AI generation outside a transaction and only wrapping the persistence section in a transaction (e.g., execute with OFF and manage a shorter transaction around the saves).
| return await this.generateTableDashboardWithAiUseCase.execute(inputData, InTransactionEnum.ON); | |
| return await this.generateTableDashboardWithAiUseCase.execute(inputData, InTransactionEnum.OFF); |
| public async implementation(inputData: GenerateTableDashboardWithAiDs): Promise<{ success: boolean }> { | ||
| const { connectionId, masterPassword, table_name, max_panels, dashboard_name } = inputData; | ||
|
|
||
| const maxPanels = max_panels ?? DEFAULT_MAX_PANELS; |
There was a problem hiding this comment.
max_panels is taken directly from input without any defensive clamping here. Even though the DTO validates this for HTTP requests, this use case can still be called programmatically with out-of-range values; consider clamping/enforcing the supported range (and handling non-positive values) before using it in prompts/slicing.
| const maxPanels = max_panels ?? DEFAULT_MAX_PANELS; | |
| const rawMaxPanels = max_panels ?? DEFAULT_MAX_PANELS; | |
| const normalizedMaxPanels = Number.isFinite(rawMaxPanels as number) ? Math.floor(rawMaxPanels as number) : DEFAULT_MAX_PANELS; | |
| const maxPanels = Math.max(1, Math.min(DEFAULT_MAX_PANELS, normalizedMaxPanels)); |
| const effectiveDashboardName = | ||
| dashboard_name || dashboardSuggestion.dashboard_name || `${table_name} Dashboard`; |
There was a problem hiding this comment.
effectiveDashboardName can come from the AI response without enforcing the DB/DTO length limits. If the underlying column is length-limited, an overlong name can cause persistence errors. Consider validating/truncating to the maximum supported length before saving.
| "suggested_panel_type": "chart" | "counter" | "table", | ||
| "suggested_chart_type": "bar" | "line" | "pie" | "doughnut" | "polarArea" | ||
| } | ||
| ] | ||
| } | ||
|
|
There was a problem hiding this comment.
This prompt’s JSON example isn’t valid JSON because it includes TypeScript-style union syntax (e.g., "chart" | "counter" | "table"). That can cause the model to emit invalid JSON that fails parsing. Use a valid JSON example and list allowed values separately (in prose) to improve reliability.
| "suggested_panel_type": "chart" | "counter" | "table", | |
| "suggested_chart_type": "bar" | "line" | "pie" | "doughnut" | "polarArea" | |
| } | |
| ] | |
| } | |
| "suggested_panel_type": "chart", | |
| "suggested_chart_type": "bar" | |
| } | |
| ] | |
| } | |
| Allowed values for "suggested_panel_type" are: "chart", "counter", "table". | |
| Allowed values for "suggested_chart_type" are: "bar", "line", "pie", "doughnut", "polarArea". |
| const aiResponse = await this.aiCoreService.completeWithProvider(AIProviderType.BEDROCK, correctionPrompt, { | ||
| temperature: 0.2, | ||
| }); |
There was a problem hiding this comment.
The AI correction call is executed on every loop iteration regardless of whether the EXPLAIN succeeded. This adds cost/latency even for already-valid queries. Consider only invoking the AI correction step when EXPLAIN fails or when you detect a specific optimization opportunity.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| })), | ||
| }; | ||
| } catch (error) { | ||
| throw new BadRequestException(`Failed to get table structure for "${table_name}": ${error.message}`); |
There was a problem hiding this comment.
This error message includes the original exception message from the underlying DAO/driver. That can leak internal DB details (table/schema names, driver errors) to clients. Prefer logging the full error server-side and returning a generic BadRequest message (or a sanitized one) to the caller.
| throw new BadRequestException(`Failed to get table structure for "${table_name}": ${error.message}`); | |
| this.logger.error( | |
| `Failed to get table structure for "${table_name}".`, | |
| error instanceof Error ? error.stack : String(error), | |
| ); | |
| throw new BadRequestException( | |
| `Failed to get table structure for "${table_name}". Please verify the table name and try again.`, | |
| ); |
| export class GenerateTableDashboardWithAiDs { | ||
| connectionId: string; | ||
| masterPassword: string; | ||
| userId: string; |
There was a problem hiding this comment.
userId is part of the data structure, but the corresponding use case implementation doesn’t read it. Consider removing it from the DS/controller payload if it’s not needed, or using it for audit/ownership checks to avoid misleading/unused fields in the API contract.
| userId: string; |