From 48faefd84432965408b79dce80a4972f0d564a5b Mon Sep 17 00:00:00 2001 From: mengnankkkk Date: Sun, 30 Aug 2026 15:28:04 +0800 Subject: [PATCH 1/3] fix: init --- .../controller/DashboardController.java | 67 +++++++ .../github/malonetalk/dto/DashboardDtos.java | 34 ++++ .../malonetalk/entity/DashboardCard.java | 31 ++++ .../malonetalk/mapper/DashboardMapper.java | 36 ++++ .../malonetalk/service/DashboardService.java | 128 +++++++++++++ .../main/resources/mapper/DashboardMapper.xml | 32 ++++ data-agent-frontend/src/api/dashboard.ts | 58 ++++++ .../src/components/layout/AppSidebar.vue | 1 + data-agent-frontend/src/router/index.ts | 10 ++ .../src/views/chat/ChatView.vue | 73 ++++++++ .../src/views/chat/components/ChatMessage.vue | 2 + .../src/views/chat/components/TracePanel.vue | 19 ++ .../views/dashboard/DashboardCardChart.vue | 139 ++++++++++++++ .../src/views/dashboard/DashboardManage.vue | 170 ++++++++++++++++++ sql/data_source.sql | 11 ++ 15 files changed, 811 insertions(+) create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/controller/DashboardController.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/dto/DashboardDtos.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/entity/DashboardCard.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/mapper/DashboardMapper.java create mode 100644 data-agent-backend/src/main/java/io/github/malonetalk/service/DashboardService.java create mode 100644 data-agent-backend/src/main/resources/mapper/DashboardMapper.xml create mode 100644 data-agent-frontend/src/api/dashboard.ts create mode 100644 data-agent-frontend/src/views/dashboard/DashboardCardChart.vue create mode 100644 data-agent-frontend/src/views/dashboard/DashboardManage.vue diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/DashboardController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DashboardController.java new file mode 100644 index 0000000..9491a92 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/DashboardController.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.controller; + +import io.github.malonetalk.agent.datasource.QueryResult; +import io.github.malonetalk.common.Result; +import io.github.malonetalk.dto.DashboardDtos.DashboardCardCreateRequest; +import io.github.malonetalk.dto.DashboardDtos.DashboardCardResponse; +import io.github.malonetalk.service.DashboardService; +import io.github.malonetalk.utils.RequestAssert; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class DashboardController { + + private final DashboardService dashboardService; + + @GetMapping("/dashboard-cards") + public Result> listCards() { + return Result.success(dashboardService.listCards()); + } + + @PostMapping("/dashboard-cards") + public Result createCard( + @Valid @RequestBody DashboardCardCreateRequest request) { + return Result.success(dashboardService.createCard(request)); + } + + @DeleteMapping("/dashboard-cards/{id}") + public Result deleteCard(@PathVariable Integer id) { + RequestAssert.requireNonNegative(id, "id must be non-negative."); + dashboardService.deleteCard(id); + return Result.success(); + } + + @PostMapping("/dashboard-cards/{id}/refresh") + public Result refreshCard(@PathVariable Integer id) { + RequestAssert.requireNonNegative(id, "id must be non-negative."); + return Result.success(dashboardService.refreshCard(id)); + } +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/DashboardDtos.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/DashboardDtos.java new file mode 100644 index 0000000..c6a5261 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/DashboardDtos.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import jakarta.validation.constraints.NotBlank; + +public final class DashboardDtos { + + private DashboardDtos() {} + + public record DashboardCardCreateRequest( + @NotBlank(message = "title cannot be blank.") String title, + Integer datasourceId, + @NotBlank(message = "sqlText cannot be blank.") String sqlText, + @NotBlank(message = "chartType cannot be blank.") String chartType) {} + + public record DashboardCardResponse( + Integer id, String title, Integer datasourceId, String sqlText, String chartType) {} +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/DashboardCard.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/DashboardCard.java new file mode 100644 index 0000000..e04c3a9 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/DashboardCard.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.entity; + +import lombok.Data; + +@Data +public class DashboardCard { + + private Integer id; + private String title; + private Integer datasourceId; + private String sqlText; + private String chartType; + private Integer creatorId; +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/DashboardMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/DashboardMapper.java new file mode 100644 index 0000000..0021bec --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/DashboardMapper.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.mapper; + +import io.github.malonetalk.entity.DashboardCard; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface DashboardMapper { + + int insertCard(DashboardCard card); + + List selectCardsByCreator(@Param("creatorId") Integer creatorId); + + DashboardCard selectCardByIdAndCreator( + @Param("id") Integer id, @Param("creatorId") Integer creatorId); + + int deleteCard(@Param("id") Integer id, @Param("creatorId") Integer creatorId); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DashboardService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DashboardService.java new file mode 100644 index 0000000..890addc --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DashboardService.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.service; + +import io.github.malonetalk.agent.datasource.QueryResult; +import io.github.malonetalk.agent.datasource.SqlExecutor; +import io.github.malonetalk.common.ErrorCode; +import io.github.malonetalk.common.UserContext; +import io.github.malonetalk.dto.DashboardDtos.DashboardCardCreateRequest; +import io.github.malonetalk.dto.DashboardDtos.DashboardCardResponse; +import io.github.malonetalk.entity.DashboardCard; +import io.github.malonetalk.entity.Datasource; +import io.github.malonetalk.exception.BusinessException; +import io.github.malonetalk.mapper.DashboardMapper; +import io.github.malonetalk.utils.RequestAssert; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class DashboardService { + + private final DashboardMapper dashboardMapper; + private final DatasourceService datasourceService; + private final SqlExecutor sqlExecutor; + + public List listCards() { + return dashboardMapper.selectCardsByCreator(currentUserId()).stream() + .map(this::toCardResponse) + .toList(); + } + + @Transactional + public DashboardCardResponse createCard(DashboardCardCreateRequest request) { + Integer userId = currentUserId(); + Datasource datasource = requireDatasource(request.datasourceId()); + String sql = sqlExecutor.validateSelectSql(request.sqlText()); + + DashboardCard card = new DashboardCard(); + card.setTitle(RequestAssert.requireNotBlank(request.title(), "title cannot be blank.")); + card.setDatasourceId(datasource.getId()); + card.setSqlText(sql); + card.setChartType(normalizeChartType(request.chartType())); + card.setCreatorId(userId); + if (dashboardMapper.insertCard(card) <= 0) { + throw BusinessException.of( + ErrorCode.OPERATION_FAILED, "Failed to save dashboard card."); + } + return toCardResponse(card); + } + + @Transactional + public void deleteCard(Integer id) { + if (dashboardMapper.deleteCard(id, currentUserId()) <= 0) { + throw cardNotFound(id); + } + } + + public QueryResult refreshCard(Integer id) { + DashboardCard card = requireCard(id); + Datasource datasource = requireDatasource(card.getDatasourceId()); + return sqlExecutor.execute(datasource, card.getSqlText()); + } + + private DashboardCard requireCard(Integer id) { + RequestAssert.requireNonNull(id, "card id cannot be null."); + DashboardCard card = dashboardMapper.selectCardByIdAndCreator(id, currentUserId()); + if (card == null) { + throw cardNotFound(id); + } + return card; + } + + private Datasource requireDatasource(Integer id) { + RequestAssert.requireNonNull(id, "datasourceId cannot be null."); + Datasource datasource = datasourceService.findById(id); + if (datasource == null) { + throw BusinessException.of(ErrorCode.RESOURCE_NOT_FOUND, "Datasource not found."); + } + return datasource; + } + + private String normalizeChartType(String chartType) { + String normalized = RequestAssert.requireNotBlank(chartType, "chartType cannot be blank."); + normalized = normalized.toLowerCase(); + return switch (normalized) { + case "table", "metric", "bar" -> normalized; + default -> + throw BusinessException.of( + ErrorCode.BAD_REQUEST, "chartType must be table, metric or bar."); + }; + } + + private Integer currentUserId() { + return UserContext.require().userId(); + } + + private DashboardCardResponse toCardResponse(DashboardCard card) { + return new DashboardCardResponse( + card.getId(), + card.getTitle(), + card.getDatasourceId(), + card.getSqlText(), + card.getChartType()); + } + + private BusinessException cardNotFound(Integer id) { + return BusinessException.of( + ErrorCode.RESOURCE_NOT_FOUND, "Dashboard card does not exist: id=" + id); + } +} diff --git a/data-agent-backend/src/main/resources/mapper/DashboardMapper.xml b/data-agent-backend/src/main/resources/mapper/DashboardMapper.xml new file mode 100644 index 0000000..170d7e1 --- /dev/null +++ b/data-agent-backend/src/main/resources/mapper/DashboardMapper.xml @@ -0,0 +1,32 @@ + + + + + + INSERT INTO dashboard_card ( + title, datasource_id, sql_text, chart_type, creator_id + ) VALUES ( + #{title}, #{datasourceId}, #{sqlText}, #{chartType}, #{creatorId} + ) + + + + + + + + DELETE FROM dashboard_card + WHERE id = #{id} + AND creator_id = #{creatorId} + + diff --git a/data-agent-frontend/src/api/dashboard.ts b/data-agent-frontend/src/api/dashboard.ts new file mode 100644 index 0000000..f3730e3 --- /dev/null +++ b/data-agent-frontend/src/api/dashboard.ts @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import request, { type ApiResponse } from './request'; + +export type ChartType = 'table' | 'metric' | 'bar'; + +export interface QueryResult { + columns: string[]; + rows: Record[]; + totalRows: number; + truncated: boolean; +} + +export interface DashboardCardResponse { + id: number; + title: string; + datasourceId: number; + sqlText: string; + chartType: ChartType; +} + +export interface DashboardCardCreateRequest { + title: string; + datasourceId: number; + sqlText: string; + chartType: ChartType; +} + +export function getDashboardCards() { + return request.get>('/dashboard-cards'); +} + +export function createDashboardCard(data: DashboardCardCreateRequest) { + return request.post>('/dashboard-cards', data); +} + +export function deleteDashboardCard(id: number) { + return request.delete>(`/dashboard-cards/${id}`); +} + +export function refreshDashboardCard(id: number) { + return request.post>(`/dashboard-cards/${id}/refresh`); +} diff --git a/data-agent-frontend/src/components/layout/AppSidebar.vue b/data-agent-frontend/src/components/layout/AppSidebar.vue index 4e0b6cd..60b7b2b 100644 --- a/data-agent-frontend/src/components/layout/AppSidebar.vue +++ b/data-agent-frontend/src/components/layout/AppSidebar.vue @@ -42,6 +42,7 @@ title: '资产管理', children: [ { path: '/asset/report', title: '报告管理' }, + { path: '/asset/dashboard', title: '看板管理' }, { path: '/asset/table-export', title: '表格导出' }, ], }, diff --git a/data-agent-frontend/src/router/index.ts b/data-agent-frontend/src/router/index.ts index 62e4818..5d78bf6 100644 --- a/data-agent-frontend/src/router/index.ts +++ b/data-agent-frontend/src/router/index.ts @@ -87,6 +87,12 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/report/ReportList.vue'), meta: { title: '报告管理' }, }, + { + path: '/asset/dashboard', + name: 'DashboardManage', + component: () => import('@/views/dashboard/DashboardManage.vue'), + meta: { title: '看板管理' }, + }, { path: '/asset/table-export', name: 'TableExportList', @@ -125,6 +131,10 @@ const routes: RouteRecordRaw[] = [ path: '/report', redirect: '/asset/report', }, + { + path: '/dashboard', + redirect: '/asset/dashboard', + }, { path: '/table-export', redirect: '/asset/table-export', diff --git a/data-agent-frontend/src/views/chat/ChatView.vue b/data-agent-frontend/src/views/chat/ChatView.vue index 9bc60bf..9b20bb3 100644 --- a/data-agent-frontend/src/views/chat/ChatView.vue +++ b/data-agent-frontend/src/views/chat/ChatView.vue @@ -18,6 +18,8 @@ diff --git a/data-agent-frontend/src/views/chat/components/ChatMessage.vue b/data-agent-frontend/src/views/chat/components/ChatMessage.vue index 3c7e281..4c721d5 100644 --- a/data-agent-frontend/src/views/chat/components/ChatMessage.vue +++ b/data-agent-frontend/src/views/chat/components/ChatMessage.vue @@ -30,6 +30,7 @@ const emit = defineEmits<{ (e: 'previewReport', content: string): void; + (e: 'saveSqlCard', sql: string): void; }>(); const SUMMARY_MARKER = '\n\nSummary:\n'; @@ -99,6 +100,7 @@ v-if="message.role === 'agent'" :message="message" @preview-report="c => emit('previewReport', c)" + @save-sql-card="sql => emit('saveSqlCard', sql)" />
diff --git a/data-agent-frontend/src/views/chat/components/TracePanel.vue b/data-agent-frontend/src/views/chat/components/TracePanel.vue index e57b44f..35e1c2d 100644 --- a/data-agent-frontend/src/views/chat/components/TracePanel.vue +++ b/data-agent-frontend/src/views/chat/components/TracePanel.vue @@ -27,6 +27,7 @@ const emit = defineEmits<{ (e: 'previewReport', content: string): void; + (e: 'saveSqlCard', sql: string): void; }>(); const isExpanded = ref(false); @@ -125,6 +126,11 @@ if (!text) return ''; return text.replace(/\\n/g, '\n'); } + + function sqlFromStep(step: TraceStep): string { + const sql = step.toolCall?.input.sql; + return typeof sql === 'string' ? sql : ''; + }