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
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export class GeneratePanelPositionWithAiDs {
dashboardId: string;
connectionId: string;
masterPassword: string;
userId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ export class GeneratedPanelPositionDto {

@ApiProperty({ description: 'Panel height in grid units' })
height: number;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing dashboard_id from GeneratedPanelPositionDto changes the response contract for generateWidgetWithAi. The frontend model GeneratedPanelWithPosition.panel_position.dashboard_id (see frontend/src/app/models/saved-query.ts) will no longer match the API and can cause type/runtime issues. Update downstream DTOs/models accordingly, or keep dashboard_id (possibly as optional) until consumers are migrated.

Suggested change
height: number;
height: number;
@ApiPropertyOptional({ description: 'Dashboard ID this panel belongs to' })
dashboard_id?: string | null;

Copilot uses AI. Check for mistakes.

@ApiProperty({ description: 'Dashboard ID', nullable: true })
dashboard_id: string | null;
}

export class GeneratedPanelWithPositionDto {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,20 +164,17 @@ export class DashboardWidgetController {
type: GeneratedPanelWithPositionDto,
})
@ApiBody({ type: GeneratePanelPositionWithAiDto })
@ApiParam({ name: 'dashboardId', required: true })
@ApiParam({ name: 'connectionId', required: true })
@UseGuards(ConnectionEditGuard)
@Timeout(TimeoutDefaults.AI)
@Post('/dashboard/:dashboardId/widget/generate/:connectionId')
@Post('/widget/generate/:connectionId')
async generateWidgetWithAi(
Comment on lines 169 to 171
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint path change is not reflected in existing consumers: frontend/src/app/services/dashboards.service.ts still calls /dashboard/${dashboardId}/widget/generate/${connectionId}. Without updating callers (or keeping a backwards-compatible alias route), the frontend will break at runtime. Please update the client(s) or add a deprecated route that forwards to the new handler.

Copilot uses AI. Check for mistakes.
@SlugUuid('connectionId') connectionId: string,
Comment on lines +170 to 172
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AI widget generation route was changed to POST /widget/generate/:connectionId, but PanelPositionModule.configure() still registers AuthMiddleware for the old path (/dashboard/:dashboardId/widget/generate/:connectionId). As a result, this endpoint may bypass AuthMiddleware and ConnectionEditGuard will likely crash because request.decoded is missing. Update the module's forRoutes list (or apply AuthMiddleware more broadly) to cover the new route.

Suggested change
@Post('/widget/generate/:connectionId')
async generateWidgetWithAi(
@SlugUuid('connectionId') connectionId: string,
@Post('/dashboard/:dashboardId/widget/generate/:connectionId')
async generateWidgetWithAi(
@SlugUuid('connectionId') connectionId: string,
@Param('dashboardId') dashboardId: string,

Copilot uses AI. Check for mistakes.
@Param('dashboardId') dashboardId: string,
@MasterPassword() masterPwd: string,
@UserId() userId: string,
@Body() generateDto: GeneratePanelPositionWithAiDto,
): Promise<GeneratedPanelWithPositionDto> {
const inputData: GeneratePanelPositionWithAiDs = {
dashboardId,
connectionId,
masterPassword: masterPwd,
userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export class GeneratePanelPositionWithAiUseCase

public async implementation(inputData: GeneratePanelPositionWithAiDs): Promise<GeneratedPanelWithPositionDto> {
const {
dashboardId,
connectionId,
masterPassword,
userId,
Expand All @@ -99,15 +98,6 @@ export class GeneratePanelPositionWithAiUseCase
throw new NotFoundException(Messages.CONNECTION_NOT_FOUND);
}

const foundDashboard = await this._dbContext.dashboardRepository.findDashboardByIdAndConnectionId(
dashboardId,
connectionId,
);

if (!foundDashboard) {
throw new NotFoundException(Messages.DASHBOARD_NOT_FOUND);
}

const dao = getDataAccessObject(foundConnection);

let userEmail: string;
Expand Down Expand Up @@ -155,7 +145,6 @@ export class GeneratePanelPositionWithAiUseCase
position_y: position_y ?? 0,
width: width ?? 6,
height: height ?? 4,
dashboard_id: dashboardId,
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ export class GenerateTableDashboardWithAiUseCase
position_y: row * PANEL_HEIGHT,
width: PANEL_WIDTH,
height: PANEL_HEIGHT,
dashboard_id: null,
},
});
} catch (error) {
Expand Down
95 changes: 6 additions & 89 deletions backend/test/ava-tests/saas-tests/dashboard-ai-widget-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ test.after(async () => {
}
});

currentTest = 'POST /dashboard/:dashboardId/widget/generate/:connectionId';
currentTest = 'POST /widget/generate/:connectionId';

test.serial(`${currentTest} should generate a widget with AI for chart type`, async (t) => {
mockResponse = MOCK_AI_RESPONSE_CHART;
Expand All @@ -183,19 +183,8 @@ test.serial(`${currentTest} should generate a widget with AI for chart type`, as
const connectionId = JSON.parse(createConnectionResponse.text).id;
t.is(createConnectionResponse.status, 201);

const createDashboard = await request(app.getHttpServer())
.post(`/dashboards/${connectionId}`)
.send({ name: 'AI Generated Dashboard' })
.set('Cookie', token)
.set('masterpwd', 'ahalaimahalai')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

const dashboardId = JSON.parse(createDashboard.text).id;
t.is(createDashboard.status, 201);

const generateWidget = await request(app.getHttpServer())
.post(`/dashboard/${dashboardId}/widget/generate/${connectionId}`)
.post(`/widget/generate/${connectionId}`)
.send({
chart_description: 'Show total sales by category as a bar chart',
})
Expand All @@ -222,7 +211,6 @@ test.serial(`${currentTest} should generate a widget with AI for chart type`, as
t.is(generateWidgetRO.panel_position.position_y, 0);
t.is(generateWidgetRO.panel_position.width, 6);
t.is(generateWidgetRO.panel_position.height, 4);
t.is(generateWidgetRO.panel_position.dashboard_id, dashboardId);

const getSavedQueries = await request(app.getHttpServer())
.get(`/connection/${connectionId}/saved-queries`)
Expand Down Expand Up @@ -253,18 +241,8 @@ test.serial(`${currentTest} should generate a counter widget with AI`, async (t)
const connectionId = JSON.parse(createConnectionResponse.text).id;
t.is(createConnectionResponse.status, 201);

const createDashboard = await request(app.getHttpServer())
.post(`/dashboards/${connectionId}`)
.send({ name: 'Counter Dashboard' })
.set('Cookie', token)
.set('masterpwd', 'ahalaimahalai')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

const dashboardId = JSON.parse(createDashboard.text).id;

const generateWidget = await request(app.getHttpServer())
.post(`/dashboard/${dashboardId}/widget/generate/${connectionId}`)
.post(`/widget/generate/${connectionId}`)
.send({
chart_description: 'Show total count of orders',
})
Expand All @@ -283,7 +261,6 @@ test.serial(`${currentTest} should generate a counter widget with AI`, async (t)
t.is(generateWidgetRO.connection_id, connectionId);

t.truthy(generateWidgetRO.panel_position);
t.is(generateWidgetRO.panel_position.dashboard_id, dashboardId);
});

test.serial(`${currentTest} should reject AI-generated unsafe query`, async (t) => {
Expand All @@ -302,18 +279,8 @@ test.serial(`${currentTest} should reject AI-generated unsafe query`, async (t)
.set('Accept', 'application/json');
const connectionId = JSON.parse(createConnectionResponse.text).id;

const createDashboard = await request(app.getHttpServer())
.post(`/dashboards/${connectionId}`)
.send({ name: 'Unsafe Query Dashboard' })
.set('Cookie', token)
.set('masterpwd', 'ahalaimahalai')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

const dashboardId = JSON.parse(createDashboard.text).id;

const generateWidget = await request(app.getHttpServer())
.post(`/dashboard/${dashboardId}/widget/generate/${connectionId}`)
.post(`/widget/generate/${connectionId}`)
.send({
chart_description: 'Delete all data',
})
Expand Down Expand Up @@ -344,19 +311,9 @@ test.serial(`${currentTest} should generate widget with custom name`, async (t)
.set('Accept', 'application/json');
const connectionId = JSON.parse(createConnectionResponse.text).id;

const createDashboard = await request(app.getHttpServer())
.post(`/dashboards/${connectionId}`)
.send({ name: 'Custom Name Dashboard' })
.set('Cookie', token)
.set('masterpwd', 'ahalaimahalai')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

const dashboardId = JSON.parse(createDashboard.text).id;

const customName = 'My Custom Widget Name';
const generateWidget = await request(app.getHttpServer())
.post(`/dashboard/${dashboardId}/widget/generate/${connectionId}`)
.post(`/widget/generate/${connectionId}`)
.send({
chart_description: 'Show sales data',
name: customName,
Expand Down Expand Up @@ -387,18 +344,8 @@ test.serial(`${currentTest} should fail without chart_description`, async (t) =>
.set('Accept', 'application/json');
const connectionId = JSON.parse(createConnectionResponse.text).id;

const createDashboard = await request(app.getHttpServer())
.post(`/dashboards/${connectionId}`)
.send({ name: 'No Description Dashboard' })
.set('Cookie', token)
.set('masterpwd', 'ahalaimahalai')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

const dashboardId = JSON.parse(createDashboard.text).id;

const generateWidget = await request(app.getHttpServer())
.post(`/dashboard/${dashboardId}/widget/generate/${connectionId}`)
.post(`/widget/generate/${connectionId}`)
.send({})
.set('Cookie', token)
.set('masterpwd', 'ahalaimahalai')
Expand All @@ -408,33 +355,3 @@ test.serial(`${currentTest} should fail without chart_description`, async (t) =>
t.is(generateWidget.status, 400);
});

test.serial(`${currentTest} should fail for non-existent dashboard`, async (t) => {
mockResponse = MOCK_AI_RESPONSE_CHART;
toolCallCounter = 0;

const connectionToTestDB = getTestData(mockFactory).connectionToPostgres;
const { token } = await registerUserAndReturnUserInfo(app);
const { testTableName } = await createTestTable(connectionToTestDB);

const createConnectionResponse = await request(app.getHttpServer())
.post('/connection')
.send(connectionToTestDB)
.set('Cookie', token)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');
const connectionId = JSON.parse(createConnectionResponse.text).id;

const fakeDashboardId = faker.string.uuid();

const generateWidget = await request(app.getHttpServer())
.post(`/dashboard/${fakeDashboardId}/widget/generate/${connectionId}`)
.send({
chart_description: 'Show some data',
})
.set('Cookie', token)
.set('masterpwd', 'ahalaimahalai')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(generateWidget.status, 404);
});
Loading