-
-
Notifications
You must be signed in to change notification settings - Fork 18
feat: add AI-driven table dashboard generation with customizable options #1631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| export class GenerateTableDashboardWithAiDs { | ||
| connectionId: string; | ||
| masterPassword: string; | ||
| userId: string; | ||
| table_name: string; | ||
| max_panels?: number; | ||
| dashboard_name?: string; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { ApiPropertyOptional } from '@nestjs/swagger'; | ||
| import { IsInt, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator'; | ||
|
|
||
| export class GenerateTableDashboardWithAiDto { | ||
| @ApiPropertyOptional({ | ||
| type: Number, | ||
| description: 'Maximum number of panels to generate (1-10)', | ||
| default: 6, | ||
| minimum: 1, | ||
| maximum: 10, | ||
| }) | ||
| @IsOptional() | ||
| @IsInt() | ||
| @Min(1) | ||
| @Max(10) | ||
| max_panels?: number; | ||
|
|
||
| @ApiPropertyOptional({ | ||
| type: String, | ||
| description: 'Optional name for the dashboard. If not provided, AI will generate one.', | ||
| }) | ||
| @IsOptional() | ||
| @IsString() | ||
| @MaxLength(255) | ||
| dashboard_name?: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,14 +8,14 @@ export class GeneratedPanelPositionDto { | |
| @ApiProperty({ description: 'Position Y in grid' }) | ||
| position_y: number; | ||
|
|
||
| @ApiProperty({ description: 'Widget width in grid units' }) | ||
| @ApiProperty({ description: 'Panel width in grid units' }) | ||
| width: number; | ||
|
|
||
| @ApiProperty({ description: 'Widget height in grid units' }) | ||
| @ApiProperty({ description: 'Panel height in grid units' }) | ||
| height: number; | ||
|
|
||
| @ApiProperty({ description: 'Dashboard ID' }) | ||
| dashboard_id: string; | ||
| @ApiProperty({ description: 'Dashboard ID', nullable: true }) | ||
| dashboard_id: string | null; | ||
| } | ||
|
|
||
| export class GeneratedPanelWithPositionDto { | ||
|
|
@@ -25,14 +25,14 @@ export class GeneratedPanelWithPositionDto { | |
| @ApiPropertyOptional({ description: 'Panel description' }) | ||
| description: string | null; | ||
|
|
||
| @ApiProperty({ description: 'Widget type', enum: DashboardWidgetTypeEnum }) | ||
| widget_type: DashboardWidgetTypeEnum; | ||
| @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; | ||
|
Comment on lines
+28
to
32
|
||
|
|
||
| @ApiPropertyOptional({ description: 'Visualization options' }) | ||
| widget_options: Record<string, unknown> | null; | ||
| panel_options: Record<string, unknown> | null; | ||
|
|
||
| @ApiProperty({ description: 'AI-generated SQL query text' }) | ||
| query_text: string; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -15,24 +15,27 @@ import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse, | |||||
| import { UseCaseType } from '../../../common/data-injection.tokens.js'; | ||||||
| import { MasterPassword } from '../../../decorators/master-password.decorator.js'; | ||||||
| import { SlugUuid } from '../../../decorators/slug-uuid.decorator.js'; | ||||||
| import { Timeout } from '../../../decorators/timeout.decorator.js'; | ||||||
| import { Timeout, TimeoutDefaults } from '../../../decorators/timeout.decorator.js'; | ||||||
| import { UserId } from '../../../decorators/user-id.decorator.js'; | ||||||
| import { InTransactionEnum } from '../../../enums/in-transaction.enum.js'; | ||||||
| import { ConnectionEditGuard } from '../../../guards/connection-edit.guard.js'; | ||||||
| import { SentryInterceptor } from '../../../interceptors/sentry.interceptor.js'; | ||||||
| import { CreatePanelPositionDs } from './data-structures/create-panel-position.ds.js'; | ||||||
| import { DeletePanelPositionDs } from './data-structures/delete-panel-position.ds.js'; | ||||||
| import { GeneratePanelPositionWithAiDs } from './data-structures/generate-panel-position-with-ai.ds.js'; | ||||||
| import { GenerateTableDashboardWithAiDs } from './data-structures/generate-table-dashboard-with-ai.ds.js'; | ||||||
| import { UpdatePanelPositionDs } from './data-structures/update-panel-position.ds.js'; | ||||||
| import { CreatePanelPositionDto } from './dto/create-panel-position.dto.js'; | ||||||
| import { FoundPanelPositionDto } from './dto/found-panel-position.dto.js'; | ||||||
| import { GeneratedPanelWithPositionDto } from './dto/generated-panel-with-position.dto.js'; | ||||||
| import { GeneratePanelPositionWithAiDto } from './dto/generate-panel-position-with-ai.dto.js'; | ||||||
| import { GenerateTableDashboardWithAiDto } from './dto/generate-table-dashboard-with-ai.dto.js'; | ||||||
| import { UpdatePanelPositionDto } from './dto/update-panel-position.dto.js'; | ||||||
| import { | ||||||
| ICreatePanelPositionWidget, | ||||||
| IDeletePanelPosition, | ||||||
| IGeneratePanelPositionWithAi, | ||||||
| IGenerateTableDashboardWithAi, | ||||||
| IUpdatePanelPosition, | ||||||
| } from './use-cases/panel-position-use-cases.interface.js'; | ||||||
|
|
||||||
|
|
@@ -52,6 +55,8 @@ export class DashboardWidgetController { | |||||
| private readonly deleteDashboardWidgetUseCase: IDeletePanelPosition, | ||||||
| @Inject(UseCaseType.GENERATE_WIDGET_WITH_AI) | ||||||
| private readonly generateWidgetWithAiUseCase: IGeneratePanelPositionWithAi, | ||||||
| @Inject(UseCaseType.GENERATE_TABLE_DASHBOARD_WITH_AI) | ||||||
| private readonly generateTableDashboardWithAiUseCase: IGenerateTableDashboardWithAi, | ||||||
| ) {} | ||||||
|
|
||||||
| @ApiOperation({ summary: 'Create a new widget in a dashboard' }) | ||||||
|
|
@@ -150,19 +155,19 @@ export class DashboardWidgetController { | |||||
| } | ||||||
|
|
||||||
| @ApiOperation({ | ||||||
| summary: 'Generate a widget using AI', | ||||||
| summary: 'Generate a panel using AI', | ||||||
| description: | ||||||
| 'Creates a new widget with SQL query and chart configuration generated by AI based on natural language description', | ||||||
| 'Creates a new panel with SQL query and chart configuration generated by AI based on natural language description', | ||||||
| }) | ||||||
| @ApiResponse({ | ||||||
| status: 201, | ||||||
| description: 'Widget configuration generated by AI (not yet saved).', | ||||||
| description: 'Panel configuration generated by AI (not yet saved).', | ||||||
| type: GeneratedPanelWithPositionDto, | ||||||
| }) | ||||||
| @ApiBody({ type: GeneratePanelPositionWithAiDto }) | ||||||
| @ApiParam({ name: 'dashboardId', required: true }) | ||||||
| @ApiParam({ name: 'connectionId', required: true }) | ||||||
| @ApiQuery({ name: 'tableName', required: true, description: 'The table name to generate the widget for' }) | ||||||
| @ApiQuery({ name: 'tableName', required: true, description: 'The table name to generate the panel for' }) | ||||||
| @UseGuards(ConnectionEditGuard) | ||||||
| @Post('/dashboard/:dashboardId/widget/generate/:connectionId') | ||||||
| async generateWidgetWithAi( | ||||||
|
|
@@ -184,4 +189,37 @@ export class DashboardWidgetController { | |||||
| }; | ||||||
| return await this.generateWidgetWithAiUseCase.execute(inputData, InTransactionEnum.OFF); | ||||||
| } | ||||||
|
|
||||||
| @ApiOperation({ | ||||||
| summary: 'Generate a full table dashboard using AI', | ||||||
| description: | ||||||
| 'Analyzes a table structure and auto-generates multiple panel configurations for a dashboard using AI', | ||||||
| }) | ||||||
| @ApiResponse({ | ||||||
| status: 201, | ||||||
| description: 'Dashboard with panels generated by AI and saved to database.', | ||||||
| }) | ||||||
| @ApiBody({ type: GenerateTableDashboardWithAiDto }) | ||||||
| @ApiParam({ name: 'connectionId', required: true }) | ||||||
| @ApiQuery({ name: 'tableName', required: true, description: 'The table name to generate the dashboard for' }) | ||||||
| @UseGuards(ConnectionEditGuard) | ||||||
| @Timeout(TimeoutDefaults.AI) | ||||||
| @Post('/dashboard/generate-table-dashboard/:connectionId') | ||||||
| async generateTableDashboardWithAi( | ||||||
| @SlugUuid('connectionId') connectionId: string, | ||||||
| @Query('tableName') tableName: string, | ||||||
| @MasterPassword() masterPwd: string, | ||||||
| @UserId() userId: string, | ||||||
| @Body() generateDto: GenerateTableDashboardWithAiDto, | ||||||
|
Comment on lines
+209
to
+213
|
||||||
| ): Promise<{ success: boolean }> { | ||||||
| const inputData: GenerateTableDashboardWithAiDs = { | ||||||
| connectionId, | ||||||
| masterPassword: masterPwd, | ||||||
| userId, | ||||||
| table_name: tableName, | ||||||
| max_panels: generateDto.max_panels, | ||||||
| dashboard_name: generateDto.dashboard_name, | ||||||
| }; | ||||||
| return await this.generateTableDashboardWithAiUseCase.execute(inputData, InTransactionEnum.ON); | ||||||
|
||||||
| return await this.generateTableDashboardWithAiUseCase.execute(inputData, InTransactionEnum.ON); | |
| return await this.generateTableDashboardWithAiUseCase.execute(inputData, InTransactionEnum.OFF); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
userIdis 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.